package session import ( "ai-agent/gateway" "ai-agent/workflow/consts/flow" flowDao "ai-agent/workflow/dao/flow" sessionDao "ai-agent/workflow/dao/session" flowDto "ai-agent/workflow/model/dto/flow" sessionDto "ai-agent/workflow/model/dto/session" "ai-agent/workflow/model/entity" flowService "ai-agent/workflow/service/flow" "context" "fmt" "sort" "strings" "gitea.redpowerfuture.com/red-future/common/beans" "gitea.redpowerfuture.com/red-future/common/utils" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" ) var SessionService = &sessionService{} type sessionService struct{} // 结果状态(与 workflow_session_result.status / VOSessionInfoResult.Status 一致) const ( resultStatusSuccess = 2 resultStatusFailed = 3 resultStatusCancel = 4 ) func (s *sessionService) List(ctx context.Context, req *sessionDto.ListSessionReq) (res *sessionDto.ListSessionRes, err error) { user, err := utils.GetUserInfo(ctx) if err != nil { return } var page *beans.Page if req.PageSize > 0 { page = &beans.Page{PageNum: req.PageNum, PageSize: req.PageSize} } list, total, err := sessionDao.SessionDao.List(ctx, user.UserName, page) if err != nil { return } res = &sessionDto.ListSessionRes{Total: total} for _, item := range list { res.List = append(res.List, &sessionDto.VOSession{ SessionId: item.SessionId, SessionName: item.SessionName, CreatedAt: item.CreatedAt, }) } return } func (s *sessionService) Delete(ctx context.Context, req *sessionDto.DeleteSessionReq) (err error) { _, err = sessionDao.SessionDao.Delete(ctx, req) return } func (s *sessionService) DeleteRecord(ctx context.Context, req *sessionDto.DeleteSessionRecordReq) (err error) { var chatIds, wfIds []int64 for _, item := range req.Ids { if item.Type == "chat" { chatIds = append(chatIds, item.Id) } else { wfIds = append(wfIds, item.Id) } } if len(chatIds) > 0 { if _, e := sessionDao.ExecChatDao.Delete(ctx, &sessionDto.DeleteExecChatReq{Id: chatIds}); e != nil { return e } } if len(wfIds) > 0 { if _, e := sessionDao.ExecWorkflowDao.Delete(ctx, &sessionDto.DeleteExecWorkflowReq{Id: wfIds}); e != nil { return e } } return } // Get 会话内结果:普通对话 + 工作流执行混排,按创建时间倒序,分页 func (s *sessionService) Get(ctx context.Context, req *sessionDto.GetSessionInfoReq) (res *sessionDto.GetSessionInfoRes, err error) { chatList, err := sessionDao.ExecChatDao.ListBySession(ctx, req.SessionId) if err != nil { return } wfList, err := sessionDao.ExecWorkflowDao.ListBySession(ctx, req.SessionId) if err != nil { return } wfResultList, err := sessionDao.ExecWorkflowResultDao.ListBySession(ctx, req.SessionId) if err != nil { return } prefix, _ := utils.GetFileAddressPrefix(ctx) // 工作流结果按 exec_id 分组,合并到对应执行记录的结果文件URL resultByExec := make(map[int64][]string) for _, wr := range wfResultList { if wr.ResultFileUrl != "" { resultByExec[wr.ExecId] = append(resultByExec[wr.ExecId], prefix+wr.ResultFileUrl) } } res = &sessionDto.GetSessionInfoRes{} for _, c := range chatList { c.ResultFileUrl = prefix + c.ResultFileUrl res.List = append(res.List, chatExecVO(c)) } for _, w := range wfList { res.List = append(res.List, workflowExecVO(w, strings.Join(resultByExec[w.Id], ","))) } sort.Slice(res.List, func(i, j int) bool { ci, cj := res.List[i].CreatedAt, res.List[j].CreatedAt if ci == nil { return false } if cj == nil { return true } return ci.After(cj) }) res.Total = len(res.List) if req.PageSize > 0 { start := int((req.PageNum - 1) * req.PageSize) if start < 0 { start = 0 } if start >= res.Total { res.List = nil return } end := start + int(req.PageSize) if end > res.Total { end = res.Total } res.List = res.List[start:end] } // 读取结果文件内容(仅 .txt)放入 ResultContent(仅当前页),供前端直接展示;路径仍保留在 ResultFileUrl for _, vo := range res.List { vo.ResultContent = readResultFileContent(ctx, vo.ResultFileUrl) } return } // readResultFileContent 读取结果 txt 文件内容(支持逗号分隔的多个 URL),仅 .txt 文件被读取,多个内容用换行连接 func readResultFileContent(ctx context.Context, fileUrl string) string { if fileUrl == "" { return "" } var parts []string for _, u := range strings.Split(fileUrl, ",") { u = strings.TrimSpace(u) if u == "" || !strings.HasSuffix(strings.ToLower(u), ".txt") { continue } fileBytes, err := gateway.GetFileBytesFromURL(ctx, u) if err != nil { glog.Warningf(ctx, "读取结果 txt 文件失败: %v", err) continue } parts = append(parts, string(fileBytes)) } return strings.Join(parts, "\n") } func chatExecVO(c *entity.ExecChat) *sessionDto.VOSessionInfoResult { status := resultStatusSuccess if c.ErrorMessage != "" { status = resultStatusFailed } return &sessionDto.VOSessionInfoResult{ Id: c.Id, Type: "chat", Status: status, RequestParams: map[string]any{"question": c.RequestParams.Question}, ResultFileUrl: c.ResultFileUrl, TotalTokens: c.TotalTokens, TotalFee: c.TotalFee, ErrorMsg: c.ErrorMessage, Error: c.Error, CreatedAt: c.CreatedAt, } } func workflowExecVO(w *entity.ExecWorkflow, resultFileUrl string) *sessionDto.VOSessionInfoResult { status := 1 if w.Status == flow.FlowExecutionStatusFailed.Code() { status = resultStatusFailed } else if w.Status == flow.FlowExecutionStatusSuccess.Code() { status = resultStatusSuccess } else if w.Status == flow.FlowExecutionStatusCancel.Code() { status = resultStatusCancel } return &sessionDto.VOSessionInfoResult{ Id: w.Id, Type: "workflow", Status: status, FlowId: w.FlowId, RequestParams: gconv.Map(w.RequestParams), ResultFileUrl: resultFileUrl, TotalTokens: w.TotalTokens, TotalFee: w.TotalFee, ErrorMsg: w.ErrorMessage, Error: w.Error, CreatedAt: w.CreatedAt, } } // ResultList 工作流执行结果树:按创建人分页查询工作流执行记录,返回按天分组的树结构(日期→流程→结果文件)。 // 分页单位为"天":每页返回 pageSize 个完整日期,同一天内的流程与文件不会被拆到不同页;pageSize 为 0 时返回全部。 func (s *sessionService) ResultList(ctx context.Context, req *sessionDto.ListWorkflowResultReq) (res *flowDto.ListFlowExecutionTreeRes, err error) { user, err := utils.GetUserInfo(ctx) if err != nil { return } dates, err := sessionDao.ExecWorkflowDao.ListDates(ctx, user.UserName, req.Page) if err != nil { return } res = &flowDto.ListFlowExecutionTreeRes{} res.ImgAddressPrefix, _ = utils.GetFileAddressPrefix(ctx) if len(dates) == 0 { return } execs, err := sessionDao.ExecWorkflowDao.ListByDates(ctx, user.UserName, dates) if err != nil { return } // 汇总本页所有执行的 id 与去重后的流程 id,一次取回结果文件与流程名 var execIds []int64 flowIdSet := make(map[int64]struct{}) for _, e := range execs { execIds = append(execIds, e.Id) flowIdSet[e.FlowId] = struct{}{} } results, err := sessionDao.ExecWorkflowResultDao.ListByExecIds(ctx, execIds) if err != nil { return } flowNameMap := make(map[int64]string) for fid := range flowIdSet { if fu, e := flowDao.FlowUserDao.Get(ctx, &flowDto.GetFlowUserReq{Id: fid}); e == nil && fu != nil && fu.FlowName != "" { flowNameMap[fid] = fu.FlowName } } resultsByExec := make(map[int64][]*entity.ExecWorkflowResult) for _, r := range results { resultsByExec[r.ExecId] = append(resultsByExec[r.ExecId], r) } execsByDate := make(map[string][]*entity.ExecWorkflow) for _, e := range execs { date := "" if e.CreatedAt != nil { date = e.CreatedAt.Format("Y-m-d") } execsByDate[date] = append(execsByDate[date], e) } prefix := res.ImgAddressPrefix for _, d := range dates { execList := execsByDate[d] if len(execList) == 0 { continue } var flows []flowDto.FlowNode for _, e := range execList { flowName := flowNameMap[e.FlowId] if flowName == "" { flowName = "工作流" } var items []flowDto.OutputItem suffixCount := make(map[string]int) for _, rf := range resultsByExec[e.Id] { if rf.ResultFileUrl == "" { continue } content := prefix + rf.ResultFileUrl ext := flowService.GetFileTypeByPath(content) suffix := outputItemSuffix(ext) suffixCount[suffix]++ items = append(items, flowDto.OutputItem{ Content: content, Type: ext, Label: fmt.Sprintf("%s_%d", suffix, suffixCount[suffix]), }) } if len(items) == 0 { continue } flows = append(flows, flowDto.FlowNode{ FlowName: flowName, Id: e.Id, SessionId: e.SessionId, Items: items, }) } if len(flows) == 0 { continue } res.Tree = append(res.Tree, flowDto.DateNode{CreateDate: d, Flows: flows}) } return } // outputItemSuffix 按文件类型映射结果项的中文标签前缀(与 flow 侧旧逻辑保持一致) func outputItemSuffix(ext string) string { switch ext { case "image": return "图片" case "video": return "视频" case "audio": return "音频" case "text": return "文案" case "html": return "HTML" default: return "内容" } } func (s *sessionService) ResultDelete(ctx context.Context, req *sessionDto.DeleteWorkflowResultReq) (err error) { _, err = sessionDao.ExecWorkflowResultDao.Delete(ctx, req) return }