diff --git a/update.sql b/update.sql index 0510525..d11733e 100644 --- a/update.sql +++ b/update.sql @@ -279,6 +279,42 @@ COMMENT ON COLUMN black_deacon_creation_info.title IS '标题'; --------------------pgsql创建creation_info表语句--------------------------- +--------------------pgsql创建black_deacon_file_temp表语句--------------------------- +-- 临时文件表 +CREATE TABLE IF NOT EXISTS black_deacon_file_temp ( + -- 基础字段(完全对齐项目规范) + 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), + + -- 业务字段 + business_id VARCHAR(255) NOT NULL DEFAULT '', + file_url VARCHAR(512) NOT NULL DEFAULT '' + ); + +-- 索引 +CREATE INDEX idx_file_temp_tenant_id ON black_deacon_file_temp(tenant_id); +CREATE INDEX idx_file_temp_business_id ON black_deacon_file_temp(business_id); +CREATE INDEX idx_file_temp_file_url ON black_deacon_file_temp(file_url); +CREATE INDEX idx_file_temp_deleted_at ON black_deacon_file_temp(deleted_at); + +-- 注释 +COMMENT ON TABLE black_deacon_file_temp IS '临时文件表'; +COMMENT ON COLUMN black_deacon_file_temp.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_file_temp.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_file_temp.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_file_temp.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_file_temp.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_file_temp.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_file_temp.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_file_temp.business_id IS '业务ID'; +COMMENT ON COLUMN black_deacon_file_temp.file_url IS '文件地址'; +--------------------pgsql创建black_deacon_file_temp表语句--------------------------- + --------------------pgsql创建black_deacon_skill_template表语句--------------------------- -- 技能模板表 CREATE TABLE IF NOT EXISTS black_deacon_skill_template ( @@ -294,14 +330,12 @@ CREATE TABLE IF NOT EXISTS black_deacon_skill_template ( -- 业务字段 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); -- 注释 @@ -315,7 +349,6 @@ 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表语句--------------------------- @@ -335,14 +368,12 @@ CREATE TABLE IF NOT EXISTS black_deacon_skill_user ( -- 业务字段 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); -- 注释 @@ -356,7 +387,6 @@ 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表语句--------------------------- diff --git a/workflow/consts/flow/flow_execution_status.go b/workflow/consts/flow/flow_execution_status.go index 9798f0a..3df7c7e 100644 --- a/workflow/consts/flow/flow_execution_status.go +++ b/workflow/consts/flow/flow_execution_status.go @@ -6,7 +6,7 @@ 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") // 暂停 + FlowExecutionStatusCancel = newFlowExecutionStatus(gconv.PtrInt8(4), "cancel") // 取消 ) type FlowExecutionStatus *int8 diff --git a/workflow/consts/public/table_name.go b/workflow/consts/public/table_name.go index 84e88c9..2242f9b 100644 --- a/workflow/consts/public/table_name.go +++ b/workflow/consts/public/table_name.go @@ -13,4 +13,5 @@ const ( TableNameFlowUser = "flow_user" TableNameSkillTemplate = "skill_template" TableNameSkillUser = "skill_user" + TableNameFileTemp = "file_temp" ) diff --git a/workflow/controller/skill/skill_user_controller.go b/workflow/controller/skill/skill_user_controller.go index 8b5014f..b78459f 100644 --- a/workflow/controller/skill/skill_user_controller.go +++ b/workflow/controller/skill/skill_user_controller.go @@ -16,11 +16,24 @@ func (c *skillUser) Create(ctx context.Context, req *skillDto.CreateSkillUserReq return skillService.SkillUserService.Create(ctx, req) } +func (c *skillUser) Update(ctx context.Context, req *skillDto.UpdateSkillUserReq) (res *beans.ResponseEmpty, err error) { + err = skillService.SkillUserService.Update(ctx, req) + return +} + 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) Get(ctx context.Context, req *skillDto.GetSkillUserReq) (res *skillDto.SkillUserVO, err error) { + return skillService.SkillUserService.Get(ctx, req) +} + +func (c *skillUser) GetUserOrTemplate(ctx context.Context, req *skillDto.GetSkillReq) (res *skillDto.SkillUserVO, err error) { + return skillService.SkillUserService.GetUserOrTemplate(ctx, req) +} + func (c *skillUser) List(ctx context.Context, req *skillDto.ListSkillReq) (res *skillDto.ListSkillUserRes, err error) { return skillService.SkillUserService.List(ctx, req) } diff --git a/workflow/dao/file/file_temp_dao.go b/workflow/dao/file/file_temp_dao.go new file mode 100644 index 0000000..6e75f67 --- /dev/null +++ b/workflow/dao/file/file_temp_dao.go @@ -0,0 +1,59 @@ +package file + +import ( + "ai-agent/workflow/consts/public" + fileDto "ai-agent/workflow/model/dto/file" + "ai-agent/workflow/model/entity" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/util/gconv" +) + +var FileTempDao = &fileTempDao{} + +type fileTempDao struct{} + +func (d *fileTempDao) Insert(ctx context.Context, req *fileDto.CreateFileTempReq) (id int64, err error) { + fileTemp := new(entity.FileTemp) + err = gconv.Struct(req, &fileTemp) + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).Insert(&fileTemp) + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *fileTempDao) BatchInsert(ctx context.Context, req []*fileDto.CreateFileTempReq) (rows int64, err error) { + var res []*entity.FileTemp + if err = gconv.Structs(req, &res); err != nil { + return + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).Data(res).Save() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *fileTempDao) Delete(ctx context.Context, req *fileDto.DeleteFileTempReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).Where(entity.FileTempCol.Id, req.Id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *fileTempDao) List(ctx context.Context, req *fileDto.ListFileTempReq, fields ...string) (res []*entity.FileTemp, total int, err error) { + model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFileTemp).NoTenantId(ctx).Fields(fields).OmitEmpty() + model.OrderDesc(entity.FileTempCol.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 index 564b156..4b462e2 100644 --- a/workflow/dao/skill/skill_template_dao.go +++ b/workflow/dao/skill/skill_template_dao.go @@ -25,6 +25,14 @@ func (d *skillTemplateDao) Insert(ctx context.Context, req *skillDto.CreateSkill return r.LastInsertId() } +func (d *skillTemplateDao) Update(ctx context.Context, req *skillDto.UpdateSkillTemplateReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).OmitEmpty().Data(&req).Where(entity.SkillTemplateCol.Id, req.Id).Update() + if err != nil { + return + } + return r.RowsAffected() +} + 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 { @@ -33,6 +41,26 @@ func (d *skillTemplateDao) Delete(ctx context.Context, req *skillDto.DeleteSkill return r.RowsAffected() } +func (d *skillTemplateDao) Count(ctx context.Context, req *skillDto.GetSkillTemplateReq) (count int, err error) { + count, err = gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).NoTenantId(ctx).OmitEmpty(). + WhereNot(entity.SkillTemplateCol.Id, req.NotInId). + Where(entity.SkillTemplateCol.Name, req.Name). + Where(entity.SkillTemplateCol.Id, req.Id).Count() + return +} + +func (d *skillTemplateDao) Get(ctx context.Context, req *skillDto.GetSkillTemplateReq, fields ...string) (res *entity.SkillTemplate, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).NoTenantId(ctx).OmitEmpty(). + Where(entity.SkillTemplateCol.Id, req.Id). + Where(entity.SkillTemplateCol.Name, req.Name). + Fields(fields).One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + 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) { diff --git a/workflow/dao/skill/skill_user_dao.go b/workflow/dao/skill/skill_user_dao.go index bf55be0..bae4fcc 100644 --- a/workflow/dao/skill/skill_user_dao.go +++ b/workflow/dao/skill/skill_user_dao.go @@ -25,6 +25,14 @@ func (d *skillUserDao) Insert(ctx context.Context, req *skillDto.CreateSkillUser return r.LastInsertId() } +func (d *skillUserDao) Update(ctx context.Context, req *skillDto.UpdateSkillUserReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).OmitEmpty().Data(&req).Where(entity.SkillUserCol.Id, req.Id).Update() + if err != nil { + return + } + return r.RowsAffected() +} + 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 { @@ -33,6 +41,28 @@ func (d *skillUserDao) Delete(ctx context.Context, req *skillDto.DeleteSkillUser return r.RowsAffected() } +func (d *skillUserDao) Count(ctx context.Context, req *skillDto.GetSkillUserReq) (count int, err error) { + count, err = gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).NoTenantId(ctx).OmitEmpty(). + Where(entity.SkillUserCol.Name, req.Name). + Where(entity.SkillUserCol.Creator, req.Creator). + WhereNot(entity.SkillUserCol.Id, req.NotInId). + Where(entity.SkillUserCol.Id, req.Id).Count() + return +} + +func (d *skillUserDao) Get(ctx context.Context, req *skillDto.GetSkillUserReq, fields ...string) (res *entity.SkillUser, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).NoTenantId(ctx).OmitEmpty(). + Where(entity.SkillUserCol.Id, req.Id). + Where(entity.SkillUserCol.Name, req.Name). + Where(entity.SkillUserCol.Creator, req.Creator). + Fields(fields).One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + 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) diff --git a/workflow/model/dto/file/file_temp_dto.go b/workflow/model/dto/file/file_temp_dto.go new file mode 100644 index 0000000..3911b06 --- /dev/null +++ b/workflow/model/dto/file/file_temp_dto.go @@ -0,0 +1,45 @@ +package file + +import ( + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type CreateFileTempReq struct { + g.Meta `path:"/create" method:"post" tags:"临时文件管理" summary:"创建临时文件" dc:"创建临时文件"` + + BusinessId string `json:"businessId"` + FileUrl string `json:"fileUrl"` +} + +type CreateFileTempRes struct { + Id int64 `json:"id,string"` +} + +type DeleteFileTempReq struct { + g.Meta `path:"/delete" method:"delete" tags:"临时文件管理" summary:"删除临时文件" dc:"删除临时文件"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type ListFileTempReq struct { + g.Meta `path:"/list" method:"get" tags:"临时文件管理" summary:"临时文件列表" dc:"临时文件列表"` + + Page *beans.Page `json:"page"` + BusinessId string `json:"businessId"` + CreatedAt *gtime.Time `json:"createdAt"` +} + +type ListFileTempRes struct { + List []*FileTempVO `json:"list"` + Total int `json:"total"` +} + +type FileTempVO struct { + Id int64 `json:"id,string" dc:"id"` + BusinessId string `json:"businessId"` + FileUrl string `json:"fileUrl"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/workflow/model/dto/flow/flow_execution_dto.go b/workflow/model/dto/flow/flow_execution_dto.go index 0c40652..5ae5841 100644 --- a/workflow/model/dto/flow/flow_execution_dto.go +++ b/workflow/model/dto/flow/flow_execution_dto.go @@ -17,6 +17,7 @@ type NodeExecutionInput struct { // FlowExecutionInput 工作流执行入参(全程不变) type FlowExecutionInput struct { + IsDialogue bool `json:"isDialogue"` ExecutionId int64 `json:"executionId"` ConfigMap map[string]*entity.FlowNode `json:"configMap"` SessionId string `json:"sessionId" dc:"会话ID"` @@ -27,15 +28,15 @@ type FlowExecutionInput struct { } 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"` + BuildType int `json:"buildType"` + 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 { @@ -143,9 +144,9 @@ type ExecuteRes struct { } type CancelReq struct { - g.Meta `path:"/cancel" method:"get" tags:"任务管理" summary:"取消任务" dc:"取消任务"` + g.Meta `path:"/cancel" method:"post" tags:"任务管理" summary:"取消任务" dc:"取消任务"` - FlowId int64 `json:"flowId" dc:"用户流程ID"` + SessionId string `json:"sessionId" dc:"会话ID"` } type CreateFlowExecutionReq struct { @@ -195,19 +196,21 @@ type ListFlowExecutionRes struct { } 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:"更新时间"` + 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"` + SessionId string `json:"sessionId" dc:"会话ID"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` + ImgAddressPrefix string `json:"imgAddressPrefix"` } // ========== 核心:构建树状结构 ========== diff --git a/workflow/model/dto/skill/skill_template_dto.go b/workflow/model/dto/skill/skill_template_dto.go index 8a80e7d..9b3e676 100644 --- a/workflow/model/dto/skill/skill_template_dto.go +++ b/workflow/model/dto/skill/skill_template_dto.go @@ -11,7 +11,6 @@ type CreateSkillTemplateReq struct { Name string `json:"name"` Description string `json:"description"` - Category string `json:"category"` FileName string `json:"fileName"` FileUrl string `json:"fileUrl"` } @@ -20,12 +19,30 @@ type CreateSkillTemplateRes struct { Id int64 `json:"id,string"` } +type UpdateSkillTemplateReq struct { + g.Meta `path:"/update" method:"put" tags:"Skill技能管理" summary:"修改Skill用户技能" dc:"修改Skill用户技能"` + + Id int64 `json:"id" v:"required#ID不能为空"` + Name string `json:"name"` + Description string `json:"description"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` +} + type DeleteSkillTemplateReq struct { g.Meta `path:"/delete" method:"delete" tags:"Skill技能管理" summary:"删除Skill技能" dc:"删除Skill技能"` Id int64 `json:"id" v:"required#ID不能为空"` } +type GetSkillTemplateReq struct { + g.Meta `path:"/get" method:"get" tags:"Skill技能管理" summary:"Skill技能详情" dc:"Skill技能详情"` + + Id int64 `json:"id"` + Name string `json:"name"` + NotInId int64 `json:"notInId"` +} + type ListSkillTemplateReq struct { g.Meta `path:"/list" method:"get" tags:"Skill技能管理" summary:"Skill技能列表" dc:"Skill技能列表"` @@ -42,7 +59,6 @@ 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:"创建时间"` diff --git a/workflow/model/dto/skill/skill_user_dto.go b/workflow/model/dto/skill/skill_user_dto.go index 6ac2d08..04e4969 100644 --- a/workflow/model/dto/skill/skill_user_dto.go +++ b/workflow/model/dto/skill/skill_user_dto.go @@ -11,7 +11,6 @@ type CreateSkillUserReq struct { Name string `json:"name"` Description string `json:"description"` - Category string `json:"category"` FileName string `json:"fileName"` FileUrl string `json:"fileUrl"` } @@ -20,12 +19,40 @@ type CreateSkillUserRes struct { Id int64 `json:"id,string"` } +type UpdateSkillUserReq struct { + g.Meta `path:"/update" method:"put" tags:"Skill用户技能管理" summary:"修改Skill用户技能" dc:"修改Skill用户技能"` + + Id int64 `json:"id" v:"required#ID不能为空"` + Name string `json:"name"` + Description string `json:"description"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` +} + type DeleteSkillUserReq struct { g.Meta `path:"/delete" method:"delete" tags:"Skill用户技能管理" summary:"删除Skill用户技能" dc:"删除Skill用户技能"` Id int64 `json:"id" v:"required#ID不能为空"` } +type GetSkillUserReq struct { + g.Meta `path:"/get" method:"get" tags:"Skill用户技能管理" summary:"Skill用户技能详情" dc:"Skill用户技能详情"` + + Id int64 `json:"id"` + Name string `json:"name"` + Creator string `json:"creator"` + NotInId int64 `json:"notInId"` +} + +type GetSkillReq struct { + g.Meta `path:"/getUserOrTemplate" method:"get" tags:"Skill用户技能管理" summary:"Skill技能详情" dc:"Skill技能详情"` + + Id int64 `json:"id"` + Name string `json:"name"` + Creator string `json:"creator"` + NotInId int64 `json:"notInId"` +} + type ListSkillReq struct { g.Meta `path:"/list" method:"get" tags:"Skill用户技能管理" summary:"Skill用户技能列表" dc:"Skill用户技能列表"` @@ -48,12 +75,12 @@ type ListSkillUserRes struct { } 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:"更新时间"` + Id int64 `json:"id,string" dc:"id"` + Name string `json:"name"` + Description string `json:"description"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` + ImgAddressPrefix string `json:"imgAddressPrefix"` } diff --git a/workflow/model/entity/file_temp.go b/workflow/model/entity/file_temp.go new file mode 100644 index 0000000..6c8f4e8 --- /dev/null +++ b/workflow/model/entity/file_temp.go @@ -0,0 +1,24 @@ +package entity + +import ( + "gitea.com/red-future/common/beans" +) + +type FileTemp struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + + BusinessId string `orm:"business_id" json:"businessId"` + FileUrl string `orm:"file_url" json:"fileUrl"` +} + +type fileTempCol struct { + beans.SQLBaseCol + BusinessId string + FileUrl string +} + +var FileTempCol = fileTempCol{ + SQLBaseCol: beans.DefSQLBaseCol, + BusinessId: "business_id", + FileUrl: "file_url", +} diff --git a/workflow/model/entity/skill_template.go b/workflow/model/entity/skill_template.go index 57a7d1a..38a60dc 100644 --- a/workflow/model/entity/skill_template.go +++ b/workflow/model/entity/skill_template.go @@ -9,7 +9,6 @@ type SkillTemplate struct { 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"` } @@ -18,7 +17,6 @@ type skillTemplateCol struct { beans.SQLBaseCol Name string Description string - Category string FileName string FileUrl string } @@ -27,7 +25,6 @@ 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 index d99ea51..0810a69 100644 --- a/workflow/model/entity/skill_user.go +++ b/workflow/model/entity/skill_user.go @@ -7,7 +7,6 @@ type SkillUser struct { 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"` } @@ -16,7 +15,6 @@ type skillUserCol struct { beans.SQLBaseCol Name string Description string - Category string FileName string FileUrl string } @@ -25,7 +23,6 @@ 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 index 6188b1c..e6144d7 100644 --- a/workflow/service/flow/flow_execution_service.go +++ b/workflow/service/flow/flow_execution_service.go @@ -3,10 +3,13 @@ package flow import ( "ai-agent/workflow/consts/flow" "ai-agent/workflow/consts/node" + fileDao "ai-agent/workflow/dao/file" flowDao "ai-agent/workflow/dao/flow" + fileDto "ai-agent/workflow/model/dto/file" flowDto "ai-agent/workflow/model/dto/flow" "ai-agent/workflow/model/entity" "context" + "errors" "fmt" "sort" "strconv" @@ -31,6 +34,10 @@ func (s *flowExecutionService) Get(ctx context.Context, req *flowDto.GetFlowExec return nil, err } res = new(flowDto.VOFlowExecution) + res.ImgAddressPrefix, err = utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } err = gconv.Struct(r, &res) return res, err } @@ -227,15 +234,61 @@ func Notify(taskId string, result any) { 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 -//} +// ===================== 核心改造:替换为 sync.Map 存储取消上下文 ===================== +var ( + // cancelMap: traceID -> context.CancelFunc + cancelMap sync.Map +) + +func (s *flowExecutionService) Cancel(ctx context.Context, req *flowDto.CancelReq) (err error) { + getRes, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ + SessionId: req.SessionId, + }) + if err != nil { + return err + } + if g.IsEmpty(getRes) { + return fmt.Errorf("会话[%s] 不存在", req.SessionId) + } + // 从 sync.Map 获取取消函数 + cancelVal, exist := cancelMap.Load(getRes.TraceId) + if !exist { + return fmt.Errorf("traceID[%s] 不存在或已执行完成", getRes.TraceId) + } + + // 执行取消 + cancel, ok := cancelVal.(context.CancelFunc) + if !ok { + return fmt.Errorf("traceID[%s] 对应的取消函数类型错误", getRes.TraceId) + } + cancel() + + // 取消后清理(可选:也可以在流程结束时统一清理) + cancelMap.Delete(getRes.TraceId) + + // 同步更新流程执行状态为已取消 + _, err = flowDao.FlowExecutionDao.Update(ctx, &flowDto.UpdateFlowExecutionReq{ + Id: getRes.Id, + Status: flow.FlowExecutionStatusCancel.Code(), + }) + if err != nil { + return fmt.Errorf("更新取消状态失败: %v", err) + } + + return nil +} func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) { + // ===================== 核心改造1:创建可取消的上下文 ===================== + execCtx, cancel := context.WithCancel(ctx) + traceId := "" + defer func() { + // 流程结束(成功/失败)时清理 cancelMap + if traceId != "" { + cancelMap.Delete(traceId) + } + cancel() + }() flowInfo, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ SessionId: req.SessionId, @@ -244,7 +297,9 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute return } var executionId int64 + var isDialogue bool if flowInfo == nil { + isDialogue = false var r = new(flowDto.CreateFlowExecutionReq) r.FlowUserId = req.FlowId r.FlowName = req.FlowName @@ -256,24 +311,55 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute span := trace.SpanFromContext(ctx) if span != nil && span.SpanContext().HasTraceID() { r.TraceId = span.SpanContext().TraceID().String() + traceId = r.TraceId + cancelMap.Store(traceId, cancel) } executionId, err = flowDao.FlowExecutionDao.Insert(ctx, r) if err != nil { return } } else { + isDialogue = true executionId = flowInfo.Id + span := trace.SpanFromContext(ctx) + if span != nil && span.SpanContext().HasTraceID() { + traceId = span.SpanContext().TraceID().String() + cancelMap.Store(traceId, cancel) + } + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + Status: flow.FlowExecutionStatusRunning.Code(), + TraceId: traceId, + } + _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) + if err != nil { + return + } } - _, err = flowDao.FlowUserDao.Update(ctx, &flowDto.UpdateFlowUserReq{ - Id: req.FlowId, - FlowContent: req.FlowContent, - NodeInputParams: req.NodeInputParams, - }) - if err != nil { - return nil, err + if !g.IsEmpty(req.FileUrl) { + createFileTempReq := make([]*fileDto.CreateFileTempReq, 0, len(req.FileUrl)) + for _, fileUrl := range req.FileUrl { + var createReq = new(fileDto.CreateFileTempReq) + createReq.BusinessId = req.SessionId + createReq.FileUrl = fileUrl + createFileTempReq = append(createFileTempReq, createReq) + } + _, err = fileDao.FileTempDao.BatchInsert(ctx, createFileTempReq) + if err != nil { + return nil, err + } } + //_, 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{ @@ -329,18 +415,18 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute // ========================================================================= // ✅【第2步】构建执行图 // ========================================================================= - runGraph, err := BuildGraphFromFlowContent(ctx, req.FlowContent, judge2IntentNodeMap, summaryNodeID) + runGraph, err := BuildGraphFromFlowContent(execCtx, req.FlowContent, judge2IntentNodeMap, summaryNodeID) if err != nil { executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, + Id: executionId, + Status: flow.FlowExecutionStatusFailed.Code(), + ErrorMessage: err.Error(), } - executionReq.Status = flow.FlowExecutionStatusFailed.Code() - executionReq.ErrorMessage = err.Error() _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) if err1 != nil { return } - return nil, err + return nil, fmt.Errorf("执行工作流失败: %v", err) } // ========================================================================= @@ -363,6 +449,7 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute // ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!) // ========================================================================= execInput := &flowDto.FlowExecutionInput{ + IsDialogue: isDialogue, ExecutionId: executionId, ConfigMap: configMap, SessionId: req.SessionId, @@ -371,18 +458,27 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute FileUrl: req.FileUrl, } // 执行工作流 - _, err = runGraph.Invoke(ctx, execInput) + _, err = runGraph.Invoke(execCtx, execInput) if err != nil { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, + // 检测是否是取消导致的错误 + if errors.Is(execCtx.Err(), context.Canceled) { + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + Status: flow.FlowExecutionStatusCancel.Code(), + } + _, _ = flowDao.FlowExecutionDao.Update(ctx, &executionReq) + return nil, fmt.Errorf("工作流已被取消: %v", err) + } + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + Status: flow.FlowExecutionStatusFailed.Code(), + ErrorMessage: err.Error(), } - executionReq.Status = flow.FlowExecutionStatusFailed.Code() - executionReq.ErrorMessage = err.Error() _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) if err1 != nil { return } - return nil, err + return nil, fmt.Errorf("执行工作流失败: %v", err) } return diff --git a/workflow/service/flow/lambda_node.go b/workflow/service/flow/lambda_node.go index 4611388..bb4fb86 100644 --- a/workflow/service/flow/lambda_node.go +++ b/workflow/service/flow/lambda_node.go @@ -3,8 +3,11 @@ package flow import ( "ai-agent/workflow/consts/flow" "ai-agent/workflow/consts/node" + "ai-agent/workflow/consts/public" + fileDao "ai-agent/workflow/dao/file" flowDao "ai-agent/workflow/dao/flow" "ai-agent/workflow/model/dto" + fileDto "ai-agent/workflow/model/dto/file" flowDto "ai-agent/workflow/model/dto/flow" "ai-agent/workflow/model/entity" "context" @@ -14,7 +17,9 @@ import ( "strings" "time" + "gitea.com/red-future/common/db/gfdb" "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) @@ -160,10 +165,15 @@ func JudgeLambda(ctx context.Context, input any) (string, error) { 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) + if !nodeInput.Global.IsDialogue { + for _, v := range *out { + contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value) + } } + if !g.IsEmpty(nodeInput.Global.Desc) { + contextParts = fmt.Sprintf("%s,%s:%s", contextParts, "描述", nodeInput.Global.Desc) + } configMap := gconv.Map(nodeInput.Config.Config) ids := gconv.Strings(configMap["branch_ids"]) branchIdNameMap := gconv.Map(configMap["branch_id_name_map"]) @@ -175,33 +185,18 @@ func JudgeLambda(ctx context.Context, input any) (string, error) { 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{ + BuildType: 2, ModelName: getIsChatModel.ModelName, SkillName: "", - IsBuild: true, Cause: "判断节点", - Form: map[string]any{}, - UserForm: map[string]any{"prompt": prompt}, + Form: map[string]any{"prompt": strings.Join(branchIdNameLines, "\n")}, + UserForm: map[string]any{"prompt": contextParts}, UserFiles: nodeInput.Global.FileUrl, SessionId: nodeInput.Global.SessionId, } @@ -209,19 +204,13 @@ func JudgeLambda(ctx context.Context, input any) (string, error) { if err != nil { return "", err } - taskResult, err := GatewayTask(ctx, msg.EpicycleId, getIsChatModel.ModelName, msg.Messages) - if err != nil { - return "", err + if g.IsEmpty(msg.Messages) { + return "", fmt.Errorf("msg is empty") } - 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]) + content = gconv.String(msg.Messages[key]) } fmt.Printf("JudgeLambda路由:目标节点ID=%s\n", gconv.String(content)) @@ -244,10 +233,16 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { outputResult = append(outputResult, field) } } + + resultUserFrom := make(map[string]any) + 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) + if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") { + if strings.Contains(field.Field, "text_content") { + field.Value = stripHtmlTags(field.Value, false) + } + resultUserFrom[field.Label] = field } } } @@ -256,13 +251,13 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { 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 !nodeInput.Global.IsDialogue { + 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{ @@ -282,31 +277,30 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { 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 结构,不输出任何额外文字" + contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:1、输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释,2、整体用
包裹,3、主标题使用

,4、章节标题使用

,5、正文段落使用

,6、列表使用

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

需要配图:N 张

N 是这条文案需要的图片数量,只能是数字,不能是其他文字,11、只输出 HTML 结构,不输出任何额外文字" resultUserFrom["prompt"] = contentStr req := flowDto.ComposeMessagesReq{ + BuildType: 1, 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 } - + if g.IsEmpty(msg.Messages) { + return nil, fmt.Errorf("msg is empty") + } taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) if err != nil { - return "", err + return nil, err } result, err := GetTaskResult(ctx, taskResult) @@ -335,7 +329,7 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { }) // 1. 去掉 HTML 标签,生成纯文本 - plainText := stripHtmlTags(content) + plainText := stripHtmlTags(content, true) // 2. 上传纯文本到 OSS textFileName := fmt.Sprintf("ai_text_%d_%d.txt", time.Now().UnixMilli(), i) textUrl, err := Upload(ctx, &dto.UploadFileBytesReq{ @@ -349,7 +343,7 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("%v:text_url:%d", nodeInput.Config.Id, i), Value: textUrl.FileURL, - Label: fmt.Sprintf("文案纯文本_%d", i), + Label: fmt.Sprintf("文案纯文本_txt_%d", i), Type: "string", Expand: extractImageCount(content), }) @@ -361,17 +355,23 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { // 从 HTML 内容里提取图片数量(例如从

需要配图:3 张

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

需要配图:(\d+) 张

`) + re := regexp.MustCompile(`

[^\d]*(\d+)[^\d]*

`) match := re.FindStringSubmatch(content) if len(match) >= 2 { num, _ := strconv.Atoi(match[1]) return num } - return 0 // 没找到默认 0 + return 0 } // stripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 -func stripHtmlTags(html string) string { +func stripHtmlTags(html string, delImageCount bool) string { + if delImageCount { + // 🔥 第一步:直接删除整个

...

标签(包含内容) + imageTagRegex := regexp.MustCompile(`

[\s\S]*?

`) + html = imageTagRegex.ReplaceAllString(html, "") + } + // 1. 替换块级标签为换行,保证排版 blockTags := regexp.MustCompile(`]*>`) text := blockTags.ReplaceAllString(html, "\n") @@ -380,11 +380,7 @@ func stripHtmlTags(html string) string { allTags := regexp.MustCompile(`<[^>]+>`) text = allTags.ReplaceAllString(text, "") - // 3. 🔥 新增:删除 "需要配图:X 张" 这一行(含前后可能的空格/换行) - imageCountLine := regexp.MustCompile(`(?m)^\s*需要配图:\d+\s*张\s*$`) - text = imageCountLine.ReplaceAllString(text, "") - - // 4. 清理多余空行(多个换行只保留一个,更干净) + // 4. 清理多余空行(多个换行只保留一个) text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n") // 5. 只去掉首尾空白,中间换行保留 @@ -429,10 +425,16 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { outputResult = append(outputResult, field) } } + + resultUserFrom := make(map[string]any) + 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) + if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") { + if strings.Contains(field.Field, "text_content") { + field.Value = stripHtmlTags(field.Value, false) + } + resultUserFrom[field.Label] = field } } } @@ -442,12 +444,13 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { } } - resultUserFrom := make(map[string]any) - for _, item := range outputResult { - resultUserFrom[item.Label] = item - } - for _, item := range nodeInput.Config.FormConfig { - resultUserFrom[item.Label] = item + if !nodeInput.Global.IsDialogue { + 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{ @@ -469,9 +472,9 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { } req := flowDto.ComposeMessagesReq{ + BuildType: 1, ModelName: nodeInput.Config.ModelConfig.ModelName, SkillName: skillName, - IsBuild: true, Cause: "图片节点", Form: resultFrom, UserForm: resultUserFrom, @@ -482,7 +485,9 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { if err != nil { return nil, err } - + if g.IsEmpty(msg.Messages) { + return nil, fmt.Errorf("msg is empty") + } taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) if err != nil { return "", err @@ -493,8 +498,6 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { 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{} @@ -548,7 +551,7 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { 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), + Label: fmt.Sprintf("图片_img_%d关联文案ID", i), Type: "string", }) } @@ -874,113 +877,51 @@ func SummaryLambda(ctx context.Context, input any) (any, error) { // 把汇总结果存入当前节点的输出 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) + err := gfdb.DB(ctx, public.DbNameBlackDeacon).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + flowInfo, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ + SessionId: execInput.Global.SessionId, + }) + if err != nil { + return err + } + + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: execInput.Global.ExecutionId, + OutputParams: summaryResult, + } + executionReq.Status = flow.FlowExecutionStatusSuccess.Code() + _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) + + if flowInfo != nil { + var url string + url, err = utils.GetFileAddressPrefix(ctx) + if err != nil { + return err + } + + createFileTempReq := make([]*fileDto.CreateFileTempReq, 0, len(flowInfo.OutputParams)) + for _, fileUrl := range flowInfo.OutputParams { + m := gconv.Map(fileUrl) + for _, v := range m { + var createReq = new(fileDto.CreateFileTempReq) + createReq.BusinessId = flowInfo.SessionId + createReq.FileUrl = url + gconv.String(v) + createFileTempReq = append(createFileTempReq, createReq) + } + } + if len(createFileTempReq) > 0 { + _, err = fileDao.FileTempDao.BatchInsert(ctx, createFileTempReq) + if err != nil { + return err + } + } + } + return nil + }) 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) diff --git a/workflow/service/flow/lambda_node_util.go b/workflow/service/flow/lambda_node_util.go index 7b58c96..c9c05ab 100644 --- a/workflow/service/flow/lambda_node_util.go +++ b/workflow/service/flow/lambda_node_util.go @@ -9,7 +9,6 @@ import ( "io" "mime/multipart" "net/http" - "strings" commonHttp "gitea.com/red-future/common/http" "gitea.com/red-future/common/utils" @@ -17,7 +16,7 @@ import ( "github.com/gogf/gf/v2/util/gconv" ) -func GetIsChatModel(ctx context.Context) (*flowDto.GetIsChatModelRes, error) { +func GetIsChatModel(ctx context.Context) (res *flowDto.GetIsChatModelRes, err error) { headers := make(map[string]string) if r := g.RequestFromCtx(ctx); r != nil { for k, v := range r.Request.Header { @@ -26,13 +25,9 @@ func GetIsChatModel(ctx context.Context) (*flowDto.GetIsChatModelRes, error) { } } } - res := new(flowDto.GetIsChatModelRes) - err := commonHttp.Get(ctx, "model-gateway/model/getIsChatModel", headers, res, nil) - if err != nil { - return nil, err - } - - return res, nil + res = new(flowDto.GetIsChatModelRes) + err = commonHttp.Get(ctx, "model-gateway/model/getIsChatModel", headers, res, nil) + return } func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, error) { @@ -53,7 +48,7 @@ func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, return res.TaskId, nil } -func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (*flowDto.ComposeMessagesRes, error) { +func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (res *flowDto.ComposeMessagesRes, err error) { headers := make(map[string]string) if r := g.RequestFromCtx(ctx); r != nil { for k, v := range r.Request.Header { @@ -62,13 +57,9 @@ func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (*flo } } } - res := new(flowDto.ComposeMessagesRes) - err := commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req) - if err != nil { - return nil, err - } - - return res, nil + res = new(flowDto.ComposeMessagesRes) + err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req) + return } func GatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (any, error) { @@ -169,77 +160,3 @@ func Upload(ctx context.Context, req *dto.UploadFileBytesReq) (*dto.UploadFileBy 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/skill/skill_user_service.go b/workflow/service/skill/skill_user_service.go index bd8a522..d944112 100644 --- a/workflow/service/skill/skill_user_service.go +++ b/workflow/service/skill/skill_user_service.go @@ -5,7 +5,11 @@ import ( skillDto "ai-agent/workflow/model/dto/skill" "ai-agent/workflow/model/entity" "context" + "fmt" + "path/filepath" + "strings" + "gitea.com/red-future/common/beans" commonHttp "gitea.com/red-future/common/http" "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/frame/g" @@ -34,25 +38,105 @@ func IsAdmin(ctx context.Context) (res bool, err error) { } func (s *skillUserService) Create(ctx context.Context, req *skillDto.CreateSkillUserReq) (res *skillDto.CreateSkillUserRes, err error) { + ext := strings.TrimPrefix(filepath.Ext(req.FileUrl), ".") + if ext != "zip" { + return nil, fmt.Errorf("文件格式不支持,请上传zip格式文件") + } admin, err := IsAdmin(ctx) if err != nil { return } var id int64 if admin { + var count int + count, err = skillDao.SkillTemplateDao.Count(ctx, &skillDto.GetSkillTemplateReq{ + Name: req.Name, + }) + if err != nil { + return nil, err + } + if count > 0 { + return nil, fmt.Errorf("技能名称 %s 已存在", req.Name) + } id, err = skillDao.SkillTemplateDao.Insert(ctx, &skillDto.CreateSkillTemplateReq{ Name: req.Name, Description: req.Description, - Category: req.Category, FileName: req.FileName, FileUrl: req.FileUrl, }) } else { + var user *beans.User + user, err = utils.GetUserInfo(ctx) + if err != nil { + return nil, err + } + var count int + count, err = skillDao.SkillUserDao.Count(ctx, &skillDto.GetSkillUserReq{ + Name: req.Name, + Creator: user.UserName, + }) + if err != nil { + return nil, err + } + if count > 0 { + return nil, fmt.Errorf("技能名称 %s 已存在", req.Name) + } id, err = skillDao.SkillUserDao.Insert(ctx, req) } return &skillDto.CreateSkillUserRes{Id: id}, err } +func (s *skillUserService) Update(ctx context.Context, req *skillDto.UpdateSkillUserReq) (err error) { + ext := strings.TrimPrefix(filepath.Ext(req.FileUrl), ".") + if ext != "zip" { + return fmt.Errorf("文件格式不支持,请上传zip格式文件") + } + admin, err := IsAdmin(ctx) + if err != nil { + return + } + if admin { + var count int + count, err = skillDao.SkillTemplateDao.Count(ctx, &skillDto.GetSkillTemplateReq{ + NotInId: req.Id, + Name: req.Name, + }) + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("技能名称 %s 已存在", req.Name) + } + _, err = skillDao.SkillTemplateDao.Update(ctx, &skillDto.UpdateSkillTemplateReq{ + Id: req.Id, + Name: req.Name, + Description: req.Description, + FileName: req.FileName, + FileUrl: req.FileUrl, + }) + } else { + var user *beans.User + user, err = utils.GetUserInfo(ctx) + if err != nil { + return err + } + var count int + count, err = skillDao.SkillUserDao.Count(ctx, &skillDto.GetSkillUserReq{ + NotInId: req.Id, + Name: req.Name, + Creator: user.UserName, + }) + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("技能名称 %s 已存在", req.Name) + } + _, err = skillDao.SkillUserDao.Update(ctx, req) + } + return err +} + func (s *skillUserService) Delete(ctx context.Context, req *skillDto.DeleteSkillUserReq) (err error) { admin, err := IsAdmin(ctx) if err != nil { @@ -68,6 +152,49 @@ func (s *skillUserService) Delete(ctx context.Context, req *skillDto.DeleteSkill return } +func (s *skillUserService) Get(ctx context.Context, req *skillDto.GetSkillUserReq) (res *skillDto.SkillUserVO, err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + if admin { + var list *entity.SkillTemplate + list, err = skillDao.SkillTemplateDao.Get(ctx, &skillDto.GetSkillTemplateReq{ + Id: req.Id, + }) + if err != nil { + return nil, err + } + res = &skillDto.SkillUserVO{} + res.ImgAddressPrefix, err = utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } + err = gconv.Struct(list, &res) + return + } + + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + req.Creator = user.UserName + list, err := skillDao.SkillUserDao.Get(ctx, &skillDto.GetSkillUserReq{ + Id: req.Id, + }) + if err != nil { + return nil, err + } + res = &skillDto.SkillUserVO{} + res.ImgAddressPrefix, err = utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } + err = gconv.Struct(list, &res) + + return +} + func (s *skillUserService) List(ctx context.Context, req *skillDto.ListSkillReq) (res *skillDto.ListSkillUserRes, err error) { admin, err := IsAdmin(ctx) if err != nil { @@ -128,3 +255,46 @@ func (s *skillUserService) ListUser(ctx context.Context, req *skillDto.ListSkill return } + +func (s *skillUserService) GetUserOrTemplate(ctx context.Context, req *skillDto.GetSkillReq) (res *skillDto.SkillUserVO, err error) { + var list *entity.SkillTemplate + list, err = skillDao.SkillTemplateDao.Get(ctx, &skillDto.GetSkillTemplateReq{ + Id: req.Id, + Name: req.Name, + }) + if err != nil { + return nil, err + } + if !g.IsEmpty(list) { + res = &skillDto.SkillUserVO{} + res.ImgAddressPrefix, err = utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } + err = gconv.Struct(list, &res) + return + } + + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + req.Creator = user.UserName + var userList *entity.SkillUser + userList, err = skillDao.SkillUserDao.Get(ctx, &skillDto.GetSkillUserReq{ + Id: req.Id, + Creator: user.UserName, + Name: req.Name, + }) + if err != nil { + return nil, err + } + res = &skillDto.SkillUserVO{} + res.ImgAddressPrefix, err = utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } + err = gconv.Struct(userList, &res) + + return +}