feat: 新增工作流执行模块

新增流程执行记录的实体、DTO、DAO、控制器和服务层,支持工作流的执行、回调及结果树状列表查询;同时更新服务名称为 ai-agent。
This commit is contained in:
2026-05-12 13:34:28 +08:00
parent 2aec7fe30f
commit 7c26914353
42 changed files with 4146 additions and 11 deletions

View File

@@ -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(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background: #fff;
color: #333;
}
.container {
max-width: 960px;
margin: 0 auto;
}
/* 图片:完全贴边,无额外间距 */
.image-block {
width: 100%;
margin: 0;
padding: 0;
}
.image-block img {
width: 100%;
height: auto;
display: block;
border-radius: 0;
}
/* 文案:极致紧凑 */
.text-block {
margin: 0;
padding: 16px; /* 仅保留内边距,不设外边距 */
line-height: 1.6;
font-size: 14px;
color: #444;
white-space: pre-wrap;
}
/* 分割线:完全去掉,改用内边距自然分隔 */
</style>
</head>
<body>
<div class="container">
`)
// 1. 先渲染图片(无任何上下边距,占满宽度)
if len(images) > 0 {
html.WriteString(`<div class="image-block">`)
for _, img := range images {
html.WriteString(fmt.Sprintf(`<img src="%s" alt="" />`, img))
}
html.WriteString(`</div>`)
}
// 2. 渲染文案(紧贴图片下方,仅用内边距留白)
if len(texts) > 0 {
html.WriteString(`<div class="text-block">`)
// 段落之间用 <br> 而不是 <br><br>,减少空行
html.WriteString(strings.Join(texts, "<br>"))
html.WriteString(`</div>`)
}
html.WriteString(`
</div>
</body>
</html>
`)
return html.String()
}