feat: add AI workflow run management with detailed audit items and UI integration
This commit is contained in:
@@ -89,10 +89,14 @@ func BuildAIWorkflowNodeSpecs(list []workflowregistry.NodeSpec) []response.AIWor
|
||||
}
|
||||
|
||||
func BuildAIWorkflowRun(item *models.AIWorkflowRun) response.AIWorkflowRunResponse {
|
||||
return BuildAIWorkflowRunWithContext(item, nil, nil, nil)
|
||||
}
|
||||
|
||||
func BuildAIWorkflowRunWithContext(item *models.AIWorkflowRun, workflow *models.AIWorkflow, version *models.AIWorkflowVersion, agent *models.AIAgent) response.AIWorkflowRunResponse {
|
||||
if item == nil {
|
||||
return response.AIWorkflowRunResponse{}
|
||||
}
|
||||
return response.AIWorkflowRunResponse{
|
||||
ret := response.AIWorkflowRunResponse{
|
||||
ID: item.ID,
|
||||
WorkflowID: item.WorkflowID,
|
||||
WorkflowVersionID: item.WorkflowVersionID,
|
||||
@@ -103,12 +107,23 @@ func BuildAIWorkflowRun(item *models.AIWorkflowRun) response.AIWorkflowRunRespon
|
||||
StatusName: workflowRunStatusName(item.Status),
|
||||
StartedAt: formatWorkflowTime(item.StartedAt),
|
||||
EndedAt: formatWorkflowTimePtr(item.EndedAt),
|
||||
DurationMS: workflowRunDurationMS(item.StartedAt, item.EndedAt),
|
||||
InterruptType: item.InterruptType,
|
||||
InterruptNodeID: item.InterruptNodeID,
|
||||
ErrorMessage: item.ErrorMessage,
|
||||
CreatedAt: formatWorkflowTime(item.CreatedAt),
|
||||
UpdatedAt: formatWorkflowTime(item.UpdatedAt),
|
||||
}
|
||||
if workflow != nil {
|
||||
ret.WorkflowName = workflow.Name
|
||||
}
|
||||
if version != nil {
|
||||
ret.WorkflowVersion = version.Version
|
||||
}
|
||||
if agent != nil {
|
||||
ret.AIAgentName = agent.Name
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func BuildAIWorkflowRunDetail(item *models.AIWorkflowRun, nodes []models.AIWorkflowNodeRun) response.AIWorkflowRunResponse {
|
||||
@@ -117,6 +132,12 @@ func BuildAIWorkflowRunDetail(item *models.AIWorkflowRun, nodes []models.AIWorkf
|
||||
return ret
|
||||
}
|
||||
|
||||
func BuildAIWorkflowRunDetailWithContext(item *models.AIWorkflowRun, nodes []models.AIWorkflowNodeRun, workflow *models.AIWorkflow, version *models.AIWorkflowVersion, agent *models.AIAgent) response.AIWorkflowRunResponse {
|
||||
ret := BuildAIWorkflowRunWithContext(item, workflow, version, agent)
|
||||
ret.Nodes = BuildAIWorkflowNodeRunList(nodes)
|
||||
return ret
|
||||
}
|
||||
|
||||
func BuildAIWorkflowRunList(list []models.AIWorkflowRun) []response.AIWorkflowRunResponse {
|
||||
ret := make([]response.AIWorkflowRunResponse, 0, len(list))
|
||||
for i := range list {
|
||||
@@ -188,3 +209,10 @@ func formatWorkflowTimePtr(value *time.Time) string {
|
||||
}
|
||||
return formatWorkflowTime(*value)
|
||||
}
|
||||
|
||||
func workflowRunDurationMS(startedAt time.Time, endedAt *time.Time) int64 {
|
||||
if startedAt.IsZero() || endedAt == nil || endedAt.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return endedAt.Sub(startedAt).Milliseconds()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package builders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
workflowregistry "agent-desk/internal/ai/workflow/registry"
|
||||
"agent-desk/internal/models"
|
||||
)
|
||||
|
||||
func TestBuildAIWorkflowNodeSpecsIncludesVariableContracts(t *testing.T) {
|
||||
@@ -30,6 +32,39 @@ func TestBuildAIWorkflowNodeSpecsIncludesVariableContracts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAIWorkflowRunIncludesAuditDisplayFields(t *testing.T) {
|
||||
startedAt := time.Date(2026, 6, 23, 10, 0, 0, 0, time.UTC)
|
||||
endedAt := startedAt.Add(1500 * time.Millisecond)
|
||||
|
||||
resp := BuildAIWorkflowRunWithContext(
|
||||
&models.AIWorkflowRun{
|
||||
ID: 9,
|
||||
WorkflowID: 11,
|
||||
WorkflowVersionID: 22,
|
||||
AIAgentID: 33,
|
||||
StartedAt: startedAt,
|
||||
EndedAt: &endedAt,
|
||||
Status: 1,
|
||||
},
|
||||
&models.AIWorkflow{Name: "售后会话流程"},
|
||||
&models.AIWorkflowVersion{Version: 3},
|
||||
&models.AIAgent{Name: "售后 Agent"},
|
||||
)
|
||||
|
||||
if resp.WorkflowName != "售后会话流程" {
|
||||
t.Fatalf("expected workflow name, got %q", resp.WorkflowName)
|
||||
}
|
||||
if resp.WorkflowVersion != 3 {
|
||||
t.Fatalf("expected workflow version 3, got %d", resp.WorkflowVersion)
|
||||
}
|
||||
if resp.AIAgentName != "售后 Agent" {
|
||||
t.Fatalf("expected agent name, got %q", resp.AIAgentName)
|
||||
}
|
||||
if resp.DurationMS != 1500 {
|
||||
t.Fatalf("expected duration 1500ms, got %d", resp.DurationMS)
|
||||
}
|
||||
}
|
||||
|
||||
func hasResponseVariable(items []workflowregistry.VariableSpec, name string) bool {
|
||||
for _, item := range items {
|
||||
if item.Name == name {
|
||||
|
||||
@@ -2,6 +2,7 @@ package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
@@ -242,7 +243,13 @@ func AIWorkflowAnyRunList(ctx *gin.Context) {
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
).Desc("id")
|
||||
list, paging := services.AIWorkflowService.FindRunPageByCnd(cnd)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAIWorkflowRunList(list), Page: paging})
|
||||
auditItems := services.AIWorkflowService.BuildRunAuditItems(list)
|
||||
results := make([]response.AIWorkflowRunResponse, 0, len(auditItems))
|
||||
for i := range auditItems {
|
||||
item := auditItems[i]
|
||||
results = append(results, builders.BuildAIWorkflowRunWithContext(&item.Run, item.Workflow, item.Version, item.Agent))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AIWorkflowGetRunBy(ctx *gin.Context) {
|
||||
@@ -259,5 +266,11 @@ func AIWorkflowGetRunBy(ctx *gin.Context) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowRunDetail(item, nodes))
|
||||
auditItems := services.AIWorkflowService.BuildRunAuditItems([]models.AIWorkflowRun{*item})
|
||||
if len(auditItems) == 0 {
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowRunDetail(item, nodes))
|
||||
return
|
||||
}
|
||||
auditItem := auditItems[0]
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowRunDetailWithContext(&auditItem.Run, nodes, auditItem.Workflow, auditItem.Version, auditItem.Agent))
|
||||
}
|
||||
|
||||
@@ -58,13 +58,17 @@ type AIWorkflowRunResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
WorkflowID int64 `json:"workflowId"`
|
||||
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||
WorkflowVersion int `json:"workflowVersion"`
|
||||
WorkflowName string `json:"workflowName"`
|
||||
ConversationID int64 `json:"conversationId"`
|
||||
AIAgentID int64 `json:"aiAgentId"`
|
||||
AIAgentName string `json:"aiAgentName"`
|
||||
MessageID int64 `json:"messageId"`
|
||||
Status int `json:"status"`
|
||||
StatusName string `json:"statusName"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
EndedAt string `json:"endedAt"`
|
||||
DurationMS int64 `json:"durationMs"`
|
||||
InterruptType string `json:"interruptType"`
|
||||
InterruptNodeID string `json:"interruptNodeId"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
|
||||
@@ -35,6 +35,13 @@ type aiWorkflowService struct {
|
||||
registry *workflowregistry.Registry
|
||||
}
|
||||
|
||||
type AIWorkflowRunAuditItem struct {
|
||||
Run models.AIWorkflowRun
|
||||
Workflow *models.AIWorkflow
|
||||
Version *models.AIWorkflowVersion
|
||||
Agent *models.AIAgent
|
||||
}
|
||||
|
||||
func (s *aiWorkflowService) Get(id int64) *models.AIWorkflow {
|
||||
if id <= 0 {
|
||||
return nil
|
||||
@@ -61,6 +68,57 @@ func (s *aiWorkflowService) FindRunPageByCnd(cnd *sqls.Cnd) (list []models.AIWor
|
||||
return repositories.AIWorkflowRunRepository.FindPageByCnd(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *aiWorkflowService) BuildRunAuditItems(list []models.AIWorkflowRun) []AIWorkflowRunAuditItem {
|
||||
ret := make([]AIWorkflowRunAuditItem, 0, len(list))
|
||||
if len(list) == 0 {
|
||||
return ret
|
||||
}
|
||||
workflowIDs := make([]int64, 0, len(list))
|
||||
versionIDs := make([]int64, 0, len(list))
|
||||
agentIDs := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
workflowIDs = appendNonZeroInt64(workflowIDs, item.WorkflowID)
|
||||
versionIDs = appendNonZeroInt64(versionIDs, item.WorkflowVersionID)
|
||||
agentIDs = appendNonZeroInt64(agentIDs, item.AIAgentID)
|
||||
}
|
||||
var workflows []models.AIWorkflow
|
||||
if len(workflowIDs) > 0 {
|
||||
workflows = repositories.AIWorkflowRepository.Find(sqls.DB(), sqls.NewCnd().In("id", workflowIDs))
|
||||
}
|
||||
var versions []models.AIWorkflowVersion
|
||||
if len(versionIDs) > 0 {
|
||||
versions = repositories.AIWorkflowVersionRepository.Find(sqls.DB(), sqls.NewCnd().In("id", versionIDs))
|
||||
}
|
||||
var agents []models.AIAgent
|
||||
if len(agentIDs) > 0 {
|
||||
agents = repositories.AIAgentRepository.Find(sqls.DB(), sqls.NewCnd().In("id", agentIDs))
|
||||
}
|
||||
workflowByID := make(map[int64]*models.AIWorkflow, len(workflows))
|
||||
for i := range workflows {
|
||||
item := workflows[i]
|
||||
workflowByID[item.ID] = &item
|
||||
}
|
||||
versionByID := make(map[int64]*models.AIWorkflowVersion, len(versions))
|
||||
for i := range versions {
|
||||
item := versions[i]
|
||||
versionByID[item.ID] = &item
|
||||
}
|
||||
agentByID := make(map[int64]*models.AIAgent, len(agents))
|
||||
for i := range agents {
|
||||
item := agents[i]
|
||||
agentByID[item.ID] = &item
|
||||
}
|
||||
for _, run := range list {
|
||||
ret = append(ret, AIWorkflowRunAuditItem{
|
||||
Run: run,
|
||||
Workflow: workflowByID[run.WorkflowID],
|
||||
Version: versionByID[run.WorkflowVersionID],
|
||||
Agent: agentByID[run.AIAgentID],
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *aiWorkflowService) GetRunDetail(id int64) (*models.AIWorkflowRun, []models.AIWorkflowNodeRun) {
|
||||
if id <= 0 {
|
||||
return nil, nil
|
||||
@@ -73,6 +131,18 @@ func (s *aiWorkflowService) GetRunDetail(id int64) (*models.AIWorkflowRun, []mod
|
||||
return run, nodes
|
||||
}
|
||||
|
||||
func appendNonZeroInt64(list []int64, value int64) []int64 {
|
||||
if value <= 0 {
|
||||
return list
|
||||
}
|
||||
for _, item := range list {
|
||||
if item == value {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, value)
|
||||
}
|
||||
|
||||
func (s *aiWorkflowService) GetByAgentID(agentID int64) *models.AIWorkflow {
|
||||
if agentID <= 0 {
|
||||
return nil
|
||||
|
||||
@@ -155,11 +155,23 @@ func TestAIWorkflowServicePublishRejectsInvalidDSL(t *testing.T) {
|
||||
func TestAIWorkflowServiceRunListAndDetail(t *testing.T) {
|
||||
setupAIWorkflowTestDB(t)
|
||||
now := time.Now()
|
||||
agent := models.AIAgent{Name: "售后 Agent", Status: enums.StatusOk}
|
||||
if err := sqls.DB().Create(&agent).Error; err != nil {
|
||||
t.Fatalf("create agent: %v", err)
|
||||
}
|
||||
workflow := models.AIWorkflow{Name: "售后流程", AgentID: agent.ID, Status: enums.StatusOk}
|
||||
if err := sqls.DB().Create(&workflow).Error; err != nil {
|
||||
t.Fatalf("create workflow: %v", err)
|
||||
}
|
||||
version := models.AIWorkflowVersion{WorkflowID: workflow.ID, Version: 7, Status: enums.StatusOk}
|
||||
if err := sqls.DB().Create(&version).Error; err != nil {
|
||||
t.Fatalf("create workflow version: %v", err)
|
||||
}
|
||||
run := models.AIWorkflowRun{
|
||||
WorkflowID: 101,
|
||||
WorkflowVersionID: 202,
|
||||
WorkflowID: workflow.ID,
|
||||
WorkflowVersionID: version.ID,
|
||||
ConversationID: 303,
|
||||
AIAgentID: 12,
|
||||
AIAgentID: agent.ID,
|
||||
MessageID: 404,
|
||||
Status: 1,
|
||||
StartedAt: now,
|
||||
@@ -169,10 +181,10 @@ func TestAIWorkflowServiceRunListAndDetail(t *testing.T) {
|
||||
t.Fatalf("create workflow run: %v", err)
|
||||
}
|
||||
otherRun := models.AIWorkflowRun{
|
||||
WorkflowID: 101,
|
||||
WorkflowVersionID: 202,
|
||||
WorkflowID: workflow.ID,
|
||||
WorkflowVersionID: version.ID,
|
||||
ConversationID: 999,
|
||||
AIAgentID: 12,
|
||||
AIAgentID: agent.ID,
|
||||
MessageID: 505,
|
||||
Status: 1,
|
||||
StartedAt: now,
|
||||
@@ -210,6 +222,19 @@ func TestAIWorkflowServiceRunListAndDetail(t *testing.T) {
|
||||
if paging.Total != 1 || len(list) != 1 || list[0].ID != run.ID {
|
||||
t.Fatalf("unexpected run list: total=%d list=%#v", paging.Total, list)
|
||||
}
|
||||
auditItems := AIWorkflowService.BuildRunAuditItems(list)
|
||||
if len(auditItems) != 1 {
|
||||
t.Fatalf("unexpected audit item count: %d", len(auditItems))
|
||||
}
|
||||
if auditItems[0].Workflow == nil || auditItems[0].Workflow.Name != workflow.Name {
|
||||
t.Fatalf("expected workflow context, got %#v", auditItems[0].Workflow)
|
||||
}
|
||||
if auditItems[0].Version == nil || auditItems[0].Version.Version != version.Version {
|
||||
t.Fatalf("expected version context, got %#v", auditItems[0].Version)
|
||||
}
|
||||
if auditItems[0].Agent == nil || auditItems[0].Agent.Name != agent.Name {
|
||||
t.Fatalf("expected agent context, got %#v", auditItems[0].Agent)
|
||||
}
|
||||
|
||||
detail, nodeRuns := AIWorkflowService.GetRunDetail(run.ID)
|
||||
if detail == nil || detail.ID != run.ID {
|
||||
|
||||
Reference in New Issue
Block a user