mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-04 19:48:02 +02:00
172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
package workflow
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"cyberstrike-ai/internal/config"
|
|
"cyberstrike-ai/internal/database"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ShouldAutoRunRoleWorkflow returns true when a role explicitly binds a workflow
|
|
// and does not turn it off. Empty policy defaults to auto to keep role UX simple.
|
|
func ShouldAutoRunRoleWorkflow(role config.RoleConfig) bool {
|
|
if strings.TrimSpace(role.WorkflowID) == "" {
|
|
return false
|
|
}
|
|
policy := strings.ToLower(strings.TrimSpace(role.WorkflowPolicy))
|
|
return policy == "" || policy == "auto"
|
|
}
|
|
|
|
// RunRoleBoundWorkflow executes the persisted role-bound workflow via cached Eino Workflow.
|
|
func RunRoleBoundWorkflow(ctx context.Context, args RunArgs) (*RunResult, error) {
|
|
if args.DB == nil {
|
|
return nil, fmt.Errorf("workflow db is nil")
|
|
}
|
|
workflowID := strings.TrimSpace(args.Role.WorkflowID)
|
|
if workflowID == "" {
|
|
return nil, fmt.Errorf("角色未绑定工作流")
|
|
}
|
|
wf, err := args.DB.GetWorkflowDefinition(workflowID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wf == nil {
|
|
return nil, fmt.Errorf("角色绑定的工作流不存在: %s", workflowID)
|
|
}
|
|
if !wf.Enabled {
|
|
return nil, fmt.Errorf("角色绑定的工作流已禁用: %s", workflowID)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
runID := uuid.NewString()
|
|
input := map[string]interface{}{
|
|
"message": args.UserMessage,
|
|
"conversationId": args.ConversationID,
|
|
"projectId": args.ProjectID,
|
|
"role": args.Role.Name,
|
|
"workflowId": wf.ID,
|
|
"workflowVersion": wf.Version,
|
|
}
|
|
inputJSON, _ := json.Marshal(input)
|
|
run := &database.WorkflowRun{
|
|
ID: runID,
|
|
WorkflowID: wf.ID,
|
|
WorkflowVersion: wf.Version,
|
|
ConversationID: args.ConversationID,
|
|
ProjectID: args.ProjectID,
|
|
RoleID: args.Role.Name,
|
|
Status: "running",
|
|
InputJSON: string(inputJSON),
|
|
StartedAt: time.Now(),
|
|
}
|
|
if err := args.DB.CreateWorkflowRun(run); err != nil {
|
|
return nil, err
|
|
}
|
|
if args.Progress != nil {
|
|
args.Progress("workflow_start", fmt.Sprintf("开始运行流程「%s」", wf.Name), map[string]interface{}{
|
|
"workflowId": wf.ID,
|
|
"workflowName": wf.Name,
|
|
"workflowVersion": wf.Version,
|
|
"workflowRunId": runID,
|
|
"conversationId": args.ConversationID,
|
|
"engine": "eino_workflow",
|
|
})
|
|
}
|
|
|
|
graph, err := parseGraph(wf.GraphJSON)
|
|
if err != nil {
|
|
_ = args.DB.FinishWorkflowRun(runID, "failed", "", err.Error())
|
|
return nil, err
|
|
}
|
|
state := newWorkflowLocalState(input, runID)
|
|
if err := executeEinoGraph(ctx, args, runID, wf.ID, wf.Version, graph, state); err != nil {
|
|
if IsAwaitingHITL(err) {
|
|
hitl := err.(*AwaitingHITLError)
|
|
partial := map[string]interface{}{
|
|
"workflowId": wf.ID,
|
|
"workflowName": wf.Name,
|
|
"workflowVersion": wf.Version,
|
|
"workflowRunId": runID,
|
|
"status": "awaiting_hitl",
|
|
"outputs": state.Outputs,
|
|
"executedNodes": state.Executed,
|
|
"skippedNodes": state.Skipped,
|
|
"pendingHitl": map[string]interface{}{
|
|
"nodeId": hitl.NodeID,
|
|
"label": hitl.NodeLabel,
|
|
"prompt": hitl.Prompt,
|
|
},
|
|
"engine": "eino_workflow",
|
|
}
|
|
partialJSON, _ := json.Marshal(partial)
|
|
_ = args.DB.SetWorkflowRunAwaitingHITL(runID, hitl.NodeID, string(partialJSON))
|
|
response := fmt.Sprintf("工作流「%s」已在节点「%s」暂停,等待人工审批。\n运行 ID:%s", wf.Name, firstNonEmpty(hitl.NodeLabel, hitl.NodeID), runID)
|
|
if args.Progress != nil {
|
|
args.Progress("workflow_paused", response, map[string]interface{}{
|
|
"workflowRunId": runID,
|
|
"status": "awaiting_hitl",
|
|
"nodeId": hitl.NodeID,
|
|
"resumeApi": fmt.Sprintf("/api/workflows/runs/%s/resume", runID),
|
|
})
|
|
}
|
|
return &RunResult{
|
|
Response: response,
|
|
RunID: runID,
|
|
Status: "awaiting_hitl",
|
|
AwaitingHITL: true,
|
|
}, nil
|
|
}
|
|
_ = args.DB.FinishWorkflowRun(runID, "failed", "", err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
output := map[string]interface{}{
|
|
"workflowId": wf.ID,
|
|
"workflowName": wf.Name,
|
|
"workflowVersion": wf.Version,
|
|
"workflowRunId": runID,
|
|
"status": "completed",
|
|
"outputs": state.Outputs,
|
|
"executedNodes": state.Executed,
|
|
"skippedNodes": state.Skipped,
|
|
"engine": "eino_workflow",
|
|
}
|
|
outputJSON, _ := json.Marshal(output)
|
|
|
|
response := renderWorkflowResponse(args.Role.Name, wf.Name, wf.Version, runID, state)
|
|
if err := args.DB.FinishWorkflowRun(runID, "completed", string(outputJSON), ""); err != nil {
|
|
return nil, err
|
|
}
|
|
if args.Progress != nil {
|
|
args.Progress("workflow_done", fmt.Sprintf("流程「%s」运行完成", wf.Name), map[string]interface{}{
|
|
"workflowRunId": runID,
|
|
"workflowId": wf.ID,
|
|
"outputs": state.Outputs,
|
|
"response": response,
|
|
"engine": "eino_workflow",
|
|
})
|
|
}
|
|
if args.Logger != nil {
|
|
args.Logger.Info("role-bound workflow completed",
|
|
zap.String("workflow_id", wf.ID),
|
|
zap.String("workflow_run_id", runID),
|
|
zap.String("conversation_id", args.ConversationID),
|
|
zap.String("role", args.Role.Name),
|
|
zap.String("engine", "eino_workflow"),
|
|
)
|
|
}
|
|
return &RunResult{Response: response, RunID: runID, Status: "completed"}, nil
|
|
}
|