feat: Implement AI Agent Workflow Binding functionality
- Added aiAgentWorkflowBindingRepository for managing workflow bindings associated with AI agents. - Enhanced agentRevisionService to include workflow bindings in agent revisions. - Updated aIAgentService to handle workflow bindings during agent creation and updates. - Introduced ai_agent_workflow_binding_service for managing workflow binding logic. - Created new API endpoints for fetching, creating, updating, and deleting AI workflows. - Developed a new dashboard page for managing AI workflows. - Updated frontend components to support workflow binding management in agent configuration. - Added necessary tests for workflow binding functionality and updated existing tests for compatibility. - Translated relevant UI texts and messages for workflow management.
This commit is contained in:
@@ -49,26 +49,42 @@ func (e *HybridEngine) Run(ctx context.Context, req RunInput) (*RunResult, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.AIAgent, req.AIConfig = snapshot.Agent, snapshot.AIConfig
|
req.AIAgent, req.AIConfig = snapshot.Agent, snapshot.AIConfig
|
||||||
if req.AIAgent.WorkflowVersionID <= 0 {
|
workflowVersionIDs := make([]int64, 0, len(snapshot.WorkflowBindings))
|
||||||
return nil, errorsx.InvalidParam("hybrid agent requires a published playbook workflow")
|
workflowTools := make([]ai.ToolDefinition, 0, len(snapshot.WorkflowBindings))
|
||||||
|
for _, binding := range snapshot.WorkflowBindings {
|
||||||
|
if binding.WorkflowVersionID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
versionAgent := req.AIAgent
|
||||||
|
versionAgent.WorkflowVersionID = binding.WorkflowVersionID
|
||||||
|
workflow, resolveErr := resolveAgentWorkflow(versionAgent)
|
||||||
|
if resolveErr != nil || !workflowvalidator.ValidateDefinition(workflow.Definition, workflowregistry.DefaultRegistry()).Valid {
|
||||||
|
return nil, errorsx.InvalidParam("hybrid agent workflow binding is invalid")
|
||||||
|
}
|
||||||
|
workflowVersionIDs = append(workflowVersionIDs, binding.WorkflowVersionID)
|
||||||
|
workflowTools = append(workflowTools, hybridWorkflowToolDefinition(binding.WorkflowVersionID, binding.ToolName, binding.TriggerInstruction))
|
||||||
}
|
}
|
||||||
workflow, err := resolveAgentWorkflow(req.AIAgent)
|
if len(workflowVersionIDs) == 0 && req.AIAgent.WorkflowVersionID > 0 {
|
||||||
if err != nil {
|
workflow, resolveErr := resolveAgentWorkflow(req.AIAgent)
|
||||||
return nil, err
|
if resolveErr != nil || !workflowvalidator.ValidateDefinition(workflow.Definition, workflowregistry.DefaultRegistry()).Valid {
|
||||||
|
return nil, errorsx.InvalidParam("hybrid agent workflow binding is invalid")
|
||||||
|
}
|
||||||
|
workflowVersionIDs = append(workflowVersionIDs, req.AIAgent.WorkflowVersionID)
|
||||||
|
workflowTools = append(workflowTools, hybridWorkflowToolDefinition(req.AIAgent.WorkflowVersionID, "", ""))
|
||||||
}
|
}
|
||||||
if result := workflowvalidator.ValidateDefinition(workflow.Definition, workflowregistry.DefaultRegistry()); !result.Valid {
|
if len(workflowVersionIDs) == 0 {
|
||||||
return nil, errorsx.InvalidParam("hybrid agent playbook validation failed")
|
return nil, errorsx.InvalidParam("hybrid agent requires a published workflow")
|
||||||
}
|
}
|
||||||
|
|
||||||
turn := e.autonomous.prepareTurn(ctx, req)
|
turn := e.autonomous.prepareTurn(ctx, req)
|
||||||
if turn.ResponsePolicy.Enforced {
|
if turn.ResponsePolicy.Enforced {
|
||||||
return writeHybridResult(req, startedAt, &ai.ChatCompletionResult{Content: turn.ResponsePolicy.ReplyText, ModelName: req.AIConfig.ModelName}, "", 0, turn.RetrieverCount, turn.RetrieveErr, turn.SkillContext, nil, turn.ResponsePolicy, nil)
|
return writeHybridResult(req, startedAt, &ai.ChatCompletionResult{Content: turn.ResponsePolicy.ReplyText, ModelName: req.AIConfig.ModelName}, "", 0, turn.RetrieverCount, turn.RetrieveErr, turn.SkillContext, nil, turn.ResponsePolicy, nil)
|
||||||
}
|
}
|
||||||
turn.SystemPrompt += "\n\nWhen a deterministic process is required, use run_playbook. Do not call it for ordinary factual questions."
|
turn.SystemPrompt += "\n\nWhen a deterministic business process is required, use the matching workflow tool. Do not call workflows for ordinary factual questions."
|
||||||
|
|
||||||
var playbookSummary *Summary
|
var playbookSummary *Summary
|
||||||
toolCalls := make([]svc.EngineToolCallInput, 0, 1)
|
toolCalls := make([]svc.EngineToolCallInput, 0, 1)
|
||||||
loop, err := e.chatWithTools(ctx, req.AIConfig, turn.SystemPrompt, turn.UserPrompt, []ai.ToolDefinition{hybridPlaybookToolDefinition(req.AIAgent.WorkflowVersionID)}, req.AIAgent.MaxSteps, func(ctx context.Context, call ai.ToolCall) (string, error) {
|
loop, err := e.chatWithTools(ctx, req.AIConfig, turn.SystemPrompt, turn.UserPrompt, workflowTools, req.AIAgent.MaxSteps, func(ctx context.Context, call ai.ToolCall) (string, error) {
|
||||||
if call.Name != "run_playbook" {
|
if call.Name != "run_playbook" {
|
||||||
return "", fmt.Errorf("unsupported hybrid tool: %s", call.Name)
|
return "", fmt.Errorf("unsupported hybrid tool: %s", call.Name)
|
||||||
}
|
}
|
||||||
@@ -79,7 +95,7 @@ func (e *HybridEngine) Run(ctx context.Context, req RunInput) (*RunResult, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if workflowVersionID != req.AIAgent.WorkflowVersionID {
|
if !containsWorkflowVersion(workflowVersionIDs, workflowVersionID) {
|
||||||
return "", fmt.Errorf("playbook is not allowed")
|
return "", fmt.Errorf("playbook is not allowed")
|
||||||
}
|
}
|
||||||
playbookDefinition := aitooling.Definition{Code: hybridPlaybookToolCode, Name: "run_playbook", RiskLevel: aitooling.RiskLevelWrite, RequireConfirmation: true, MaxCallsPerRun: 1}
|
playbookDefinition := aitooling.Definition{Code: hybridPlaybookToolCode, Name: "run_playbook", RiskLevel: aitooling.RiskLevelWrite, RequireConfirmation: true, MaxCallsPerRun: 1}
|
||||||
@@ -93,7 +109,9 @@ func (e *HybridEngine) Run(ctx context.Context, req RunInput) (*RunResult, error
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
callStartedAt := time.Now()
|
callStartedAt := time.Now()
|
||||||
playbookSummary, err = e.workflow.Run(ctx, req)
|
workflowReq := req
|
||||||
|
workflowReq.AIAgent.WorkflowVersionID = workflowVersionID
|
||||||
|
playbookSummary, err = e.workflow.Run(ctx, workflowReq)
|
||||||
toolRecord := svc.EngineToolCallInput{ToolCode: hybridPlaybookToolCode, RiskLevel: "write", RequireConfirm: true, ArgumentsPreview: call.Arguments, DurationMS: int(time.Since(callStartedAt).Milliseconds())}
|
toolRecord := svc.EngineToolCallInput{ToolCode: hybridPlaybookToolCode, RiskLevel: "write", RequireConfirm: true, ArgumentsPreview: call.Arguments, DurationMS: int(time.Since(callStartedAt).Milliseconds())}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
toolRecord.Status, toolRecord.ErrorMessage = "failed", err.Error()
|
toolRecord.Status, toolRecord.ErrorMessage = "failed", err.Error()
|
||||||
@@ -143,12 +161,28 @@ func (e *HybridEngine) Resume(ctx context.Context, req ResumeInput) (*RunResult,
|
|||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hybridPlaybookToolDefinition(workflowVersionID int64) ai.ToolDefinition {
|
func hybridWorkflowToolDefinition(workflowVersionID int64, toolName, instruction string) ai.ToolDefinition {
|
||||||
return ai.ToolDefinition{Name: "run_playbook", Description: "Run the Agent's published deterministic Playbook when the customer needs a controlled business action.", Parameters: map[string]any{
|
description := "Run this Agent's published deterministic workflow when the customer needs the controlled business action."
|
||||||
"type": "object", "properties": map[string]any{"workflowVersionId": map[string]any{"type": "integer", "description": "The bound Playbook version."}}, "required": []string{"workflowVersionId"},
|
if strings.TrimSpace(toolName) != "" {
|
||||||
|
description += " Workflow: " + toolName + "."
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(instruction) != "" {
|
||||||
|
description += " Use when: " + instruction
|
||||||
|
}
|
||||||
|
return ai.ToolDefinition{Name: "run_playbook", Description: description, Parameters: map[string]any{
|
||||||
|
"type": "object", "properties": map[string]any{"workflowVersionId": map[string]any{"type": "integer", "description": fmt.Sprintf("The allowed workflow version (%d).", workflowVersionID)}}, "required": []string{"workflowVersionId"},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsWorkflowVersion(items []int64, value int64) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if item == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func parseHybridPlaybookCall(raw string) (int64, error) {
|
func parseHybridPlaybookCall(raw string) (int64, error) {
|
||||||
var input struct {
|
var input struct {
|
||||||
WorkflowVersionID int64 `json:"workflowVersionId"`
|
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||||
|
|||||||
@@ -233,6 +233,11 @@ func registerDashboardAIAgentRoutes(group *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func registerDashboardAIWorkflowRoutes(group *gin.RouterGroup) {
|
func registerDashboardAIWorkflowRoutes(group *gin.RouterGroup) {
|
||||||
|
group.GET("/:id", dashboard.AIWorkflowGetBy)
|
||||||
|
group.POST("/create", dashboard.AIWorkflowPostCreate)
|
||||||
|
group.POST("/update", dashboard.AIWorkflowPostUpdate)
|
||||||
|
group.POST("/delete", dashboard.AIWorkflowPostDelete)
|
||||||
|
group.Any("/list", dashboard.AIWorkflowAnyList)
|
||||||
group.GET("/node-spec/list", dashboard.AIWorkflowGetNodeSpecList)
|
group.GET("/node-spec/list", dashboard.AIWorkflowGetNodeSpecList)
|
||||||
group.GET("/default-definition", dashboard.AIWorkflowGetDefaultDefinition)
|
group.GET("/default-definition", dashboard.AIWorkflowGetDefaultDefinition)
|
||||||
group.GET("/template/list", dashboard.AIWorkflowGetTemplateList)
|
group.GET("/template/list", dashboard.AIWorkflowGetTemplateList)
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons
|
|||||||
Skills: make([]response.AIAgentSkillResponse, 0),
|
Skills: make([]response.AIAgentSkillResponse, 0),
|
||||||
Teams: make([]response.AIAgentTeamResponse, 0),
|
Teams: make([]response.AIAgentTeamResponse, 0),
|
||||||
DirectTools: make([]response.AIAgentMCPToolResponse, 0),
|
DirectTools: make([]response.AIAgentMCPToolResponse, 0),
|
||||||
|
WorkflowBindings: make([]response.AIAgentWorkflowBindingResponse, 0),
|
||||||
WorkflowVersionID: item.WorkflowVersionID,
|
WorkflowVersionID: item.WorkflowVersionID,
|
||||||
PublishedRevisionID: item.PublishedRevisionID,
|
PublishedRevisionID: item.PublishedRevisionID,
|
||||||
WorkflowPublished: item.WorkflowVersionID > 0,
|
WorkflowPublished: item.WorkflowVersionID > 0,
|
||||||
@@ -348,6 +349,16 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, binding := range services.AIAgentService.ListWorkflowBindings(item.ID) {
|
||||||
|
if binding.Workflow == nil || binding.Version == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret.WorkflowBindings = append(ret.WorkflowBindings, response.AIAgentWorkflowBindingResponse{
|
||||||
|
ID: binding.Binding.ID, WorkflowID: binding.Binding.WorkflowID, WorkflowVersionID: binding.Binding.WorkflowVersionID,
|
||||||
|
WorkflowName: binding.Workflow.Name, WorkflowVersion: binding.Version.Version, ToolName: binding.Binding.ToolName,
|
||||||
|
TriggerInstruction: binding.Binding.TriggerInstruction, Priority: binding.Binding.Priority, Enabled: binding.Binding.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ var Models = []any{
|
|||||||
&AgentRunQualityFeedback{},
|
&AgentRunQualityFeedback{},
|
||||||
&AIWorkflow{},
|
&AIWorkflow{},
|
||||||
&AIWorkflowVersion{},
|
&AIWorkflowVersion{},
|
||||||
|
&AIAgentWorkflowBinding{},
|
||||||
&AIWorkflowRun{},
|
&AIWorkflowRun{},
|
||||||
&AIWorkflowNodeRun{},
|
&AIWorkflowNodeRun{},
|
||||||
&ConversationInterrupt{},
|
&ConversationInterrupt{},
|
||||||
@@ -664,6 +665,21 @@ type AIWorkflowVersion struct {
|
|||||||
AuditFields
|
AuditFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AIAgentWorkflowBinding binds an Agent to one immutable, published workflow
|
||||||
|
// version. Workflows are maintained independently and can be reused by many
|
||||||
|
// Agents; Agent publication snapshots the binding set for reproducible runs.
|
||||||
|
type AIAgentWorkflowBinding struct {
|
||||||
|
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
AIAgentID int64 `gorm:"type:bigint;not null;index;uniqueIndex:uk_agent_workflow_binding"`
|
||||||
|
WorkflowID int64 `gorm:"type:bigint;not null;index"`
|
||||||
|
WorkflowVersionID int64 `gorm:"type:bigint;not null;index;uniqueIndex:uk_agent_workflow_binding"`
|
||||||
|
ToolName string `gorm:"type:varchar(100);not null;default:''"`
|
||||||
|
TriggerInstruction string `gorm:"type:text"`
|
||||||
|
Priority int `gorm:"type:int;not null;default:0;index"`
|
||||||
|
Enabled bool `gorm:"not null;default:true;index"`
|
||||||
|
AuditFields
|
||||||
|
}
|
||||||
|
|
||||||
// AIWorkflowRun 表示一次会话 workflow 执行记录。
|
// AIWorkflowRun 表示一次会话 workflow 执行记录。
|
||||||
type AIWorkflowRun struct {
|
type AIWorkflowRun struct {
|
||||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ type AIAgentMCPToolRequest struct {
|
|||||||
Arguments map[string]string `json:"arguments"`
|
Arguments map[string]string `json:"arguments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AIAgentWorkflowBindingRequest struct {
|
||||||
|
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||||
|
ToolName string `json:"toolName"`
|
||||||
|
TriggerInstruction string `json:"triggerInstruction"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateAIConfigRequest struct {
|
type CreateAIConfigRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Provider enums.AIProvider `json:"provider"`
|
Provider enums.AIProvider `json:"provider"`
|
||||||
@@ -63,6 +71,7 @@ type CreateAIAgentRequest struct {
|
|||||||
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
|
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
|
||||||
SkillIDs []int64 `json:"skillIds"`
|
SkillIDs []int64 `json:"skillIds"`
|
||||||
DirectTools []AIAgentMCPToolRequest `json:"directTools"`
|
DirectTools []AIAgentMCPToolRequest `json:"directTools"`
|
||||||
|
WorkflowBindings []AIAgentWorkflowBindingRequest `json:"workflowBindings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAIAgentRequest struct {
|
type UpdateAIAgentRequest struct {
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ type AIAgentMCPToolResponse struct {
|
|||||||
Arguments map[string]string `json:"arguments"`
|
Arguments map[string]string `json:"arguments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AIAgentWorkflowBindingResponse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
WorkflowID int64 `json:"workflowId"`
|
||||||
|
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||||
|
WorkflowName string `json:"workflowName"`
|
||||||
|
WorkflowVersion int `json:"workflowVersion"`
|
||||||
|
ToolName string `json:"toolName"`
|
||||||
|
TriggerInstruction string `json:"triggerInstruction"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentRevisionResponse struct {
|
type AgentRevisionResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
AgentID int64 `json:"agentId"`
|
AgentID int64 `json:"agentId"`
|
||||||
@@ -79,44 +91,45 @@ func BuildAIConfigResponse(item *models.AIConfig) AIConfigResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AIAgentResponse struct {
|
type AIAgentResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Status enums.Status `json:"status"`
|
Status enums.Status `json:"status"`
|
||||||
StatusName string `json:"statusName"`
|
StatusName string `json:"statusName"`
|
||||||
AIConfigID int64 `json:"aiConfigId"`
|
AIConfigID int64 `json:"aiConfigId"`
|
||||||
AIConfigName string `json:"aiConfigName"`
|
AIConfigName string `json:"aiConfigName"`
|
||||||
RuntimeMode enums.AIAgentRuntimeMode `json:"runtimeMode"`
|
RuntimeMode enums.AIAgentRuntimeMode `json:"runtimeMode"`
|
||||||
RuntimeModeName string `json:"runtimeModeName"`
|
RuntimeModeName string `json:"runtimeModeName"`
|
||||||
MaxSteps int `json:"maxSteps"`
|
MaxSteps int `json:"maxSteps"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
ContextWindow int `json:"contextWindow"`
|
||||||
ToolPolicy string `json:"toolPolicy"`
|
ToolPolicy string `json:"toolPolicy"`
|
||||||
KnowledgePolicy string `json:"knowledgePolicy"`
|
KnowledgePolicy string `json:"knowledgePolicy"`
|
||||||
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
|
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
|
||||||
ServiceModeName string `json:"serviceModeName"`
|
ServiceModeName string `json:"serviceModeName"`
|
||||||
SystemPrompt string `json:"systemPrompt"`
|
SystemPrompt string `json:"systemPrompt"`
|
||||||
WelcomeMessage string `json:"welcomeMessage"`
|
WelcomeMessage string `json:"welcomeMessage"`
|
||||||
ReplyTimeoutSeconds int `json:"replyTimeoutSeconds"`
|
ReplyTimeoutSeconds int `json:"replyTimeoutSeconds"`
|
||||||
RolloutPercent int `json:"rolloutPercent"`
|
RolloutPercent int `json:"rolloutPercent"`
|
||||||
PreviousRolloutPercent int `json:"previousRolloutPercent"`
|
PreviousRolloutPercent int `json:"previousRolloutPercent"`
|
||||||
Teams []AIAgentTeamResponse `json:"teams"`
|
Teams []AIAgentTeamResponse `json:"teams"`
|
||||||
HandoffMode enums.AIAgentHandoffMode `json:"handoffMode"`
|
HandoffMode enums.AIAgentHandoffMode `json:"handoffMode"`
|
||||||
HandoffModeName string `json:"handoffModeName"`
|
HandoffModeName string `json:"handoffModeName"`
|
||||||
FallbackMode enums.AIAgentFallbackMode `json:"fallbackMode"`
|
FallbackMode enums.AIAgentFallbackMode `json:"fallbackMode"`
|
||||||
FallbackModeName string `json:"fallbackModeName"`
|
FallbackModeName string `json:"fallbackModeName"`
|
||||||
FallbackMessage string `json:"fallbackMessage"`
|
FallbackMessage string `json:"fallbackMessage"`
|
||||||
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
|
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
|
||||||
SkillIDs []int64 `json:"skillIds"`
|
SkillIDs []int64 `json:"skillIds"`
|
||||||
Skills []AIAgentSkillResponse `json:"skills"`
|
Skills []AIAgentSkillResponse `json:"skills"`
|
||||||
DirectTools []AIAgentMCPToolResponse `json:"directTools"`
|
DirectTools []AIAgentMCPToolResponse `json:"directTools"`
|
||||||
WorkflowVersionID int64 `json:"workflowVersionId"`
|
WorkflowBindings []AIAgentWorkflowBindingResponse `json:"workflowBindings"`
|
||||||
PublishedRevisionID int64 `json:"publishedRevisionId"`
|
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||||
WorkflowPublished bool `json:"workflowPublished"`
|
PublishedRevisionID int64 `json:"publishedRevisionId"`
|
||||||
WorkflowState string `json:"workflowState"`
|
WorkflowPublished bool `json:"workflowPublished"`
|
||||||
WorkflowStateText string `json:"workflowStateText"`
|
WorkflowState string `json:"workflowState"`
|
||||||
SortNo int `json:"sortNo"`
|
WorkflowStateText string `json:"workflowStateText"`
|
||||||
CreatedAt string `json:"createdAt"`
|
SortNo int `json:"sortNo"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
CreateUserName string `json:"createUserName"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
UpdateUserName string `json:"updateUserName"`
|
CreateUserName string `json:"createUserName"`
|
||||||
|
UpdateUserName string `json:"updateUserName"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"agent-desk/internal/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AIAgentWorkflowBindingRepository = newAIAgentWorkflowBindingRepository()
|
||||||
|
|
||||||
|
func newAIAgentWorkflowBindingRepository() *aiAgentWorkflowBindingRepository {
|
||||||
|
return &aiAgentWorkflowBindingRepository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type aiAgentWorkflowBindingRepository struct{}
|
||||||
|
|
||||||
|
func (r *aiAgentWorkflowBindingRepository) FindByAgentID(db *gorm.DB, agentID int64) []models.AIAgentWorkflowBinding {
|
||||||
|
ret := make([]models.AIAgentWorkflowBinding, 0)
|
||||||
|
if agentID > 0 {
|
||||||
|
db.Where("ai_agent_id = ?", agentID).Order("priority ASC, id ASC").Find(&ret)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *aiAgentWorkflowBindingRepository) FindEnabledByAgentID(db *gorm.DB, agentID int64) []models.AIAgentWorkflowBinding {
|
||||||
|
ret := make([]models.AIAgentWorkflowBinding, 0)
|
||||||
|
if agentID > 0 {
|
||||||
|
db.Where("ai_agent_id = ? AND enabled = ?", agentID, true).Order("priority ASC, id ASC").Find(&ret)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *aiAgentWorkflowBindingRepository) CountByWorkflowID(db *gorm.DB, workflowID int64) int64 {
|
||||||
|
var count int64
|
||||||
|
if workflowID > 0 {
|
||||||
|
db.Model(&models.AIAgentWorkflowBinding{}).Where("workflow_id = ?", workflowID).Count(&count)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *aiAgentWorkflowBindingRepository) ReplaceByAgentID(db *gorm.DB, agentID int64, items []models.AIAgentWorkflowBinding) error {
|
||||||
|
if err := db.Where("ai_agent_id = ?", agentID).Delete(&models.AIAgentWorkflowBinding{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.Create(&items).Error
|
||||||
|
}
|
||||||
@@ -38,10 +38,19 @@ func (s *agentRevisionService) FindByAgentID(agentID int64) []models.AgentRevisi
|
|||||||
}
|
}
|
||||||
|
|
||||||
type agentRevisionDefinition struct {
|
type agentRevisionDefinition struct {
|
||||||
Agent agentRevisionAgent `json:"agent"`
|
Agent agentRevisionAgent `json:"agent"`
|
||||||
Model agentRevisionModel `json:"model"`
|
Model agentRevisionModel `json:"model"`
|
||||||
WorkflowVersionID int64 `json:"workflowVersionId"`
|
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||||
WorkflowDefinition string `json:"workflowDefinition"`
|
WorkflowDefinition string `json:"workflowDefinition"`
|
||||||
|
WorkflowBindings []agentRevisionWorkflowBinding `json:"workflowBindings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentRevisionWorkflowBinding struct {
|
||||||
|
WorkflowID int64 `json:"workflowId"`
|
||||||
|
WorkflowVersionID int64 `json:"workflowVersionId"`
|
||||||
|
ToolName string `json:"toolName"`
|
||||||
|
TriggerInstruction string `json:"triggerInstruction"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// agentRevisionModel deliberately excludes APIKey. A revision must capture
|
// agentRevisionModel deliberately excludes APIKey. A revision must capture
|
||||||
@@ -84,9 +93,10 @@ type agentRevisionAgent struct {
|
|||||||
// a published revision. Model credentials deliberately remain on the current
|
// a published revision. Model credentials deliberately remain on the current
|
||||||
// AIConfig so credential rotation does not require republishing every Agent.
|
// AIConfig so credential rotation does not require republishing every Agent.
|
||||||
type AgentRevisionSnapshot struct {
|
type AgentRevisionSnapshot struct {
|
||||||
Revision models.AgentRevision
|
Revision models.AgentRevision
|
||||||
Agent models.AIAgent
|
Agent models.AIAgent
|
||||||
AIConfig models.AIConfig
|
AIConfig models.AIConfig
|
||||||
|
WorkflowBindings []agentRevisionWorkflowBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvePublishedSnapshot restores a published Agent revision for runtime
|
// ResolvePublishedSnapshot restores a published Agent revision for runtime
|
||||||
@@ -112,6 +122,7 @@ func (s *agentRevisionService) ResolvePublishedSnapshot(agent models.AIAgent, co
|
|||||||
return nil, errorsx.InvalidParam("published agent model config no longer matches")
|
return nil, errorsx.InvalidParam("published agent model config no longer matches")
|
||||||
}
|
}
|
||||||
applyRevisionAgentSnapshot(&snapshot.Agent, definition.Agent)
|
applyRevisionAgentSnapshot(&snapshot.Agent, definition.Agent)
|
||||||
|
snapshot.WorkflowBindings = append([]agentRevisionWorkflowBinding(nil), definition.WorkflowBindings...)
|
||||||
if definition.WorkflowVersionID > 0 {
|
if definition.WorkflowVersionID > 0 {
|
||||||
snapshot.Agent.WorkflowVersionID = definition.WorkflowVersionID
|
snapshot.Agent.WorkflowVersionID = definition.WorkflowVersionID
|
||||||
}
|
}
|
||||||
@@ -196,6 +207,9 @@ func (s *agentRevisionService) publishSnapshot(db *gorm.DB, agent *models.AIAgen
|
|||||||
WorkflowVersionID: workflowVersionID,
|
WorkflowVersionID: workflowVersionID,
|
||||||
WorkflowDefinition: workflowDefinition,
|
WorkflowDefinition: workflowDefinition,
|
||||||
}
|
}
|
||||||
|
for _, binding := range repositories.AIAgentWorkflowBindingRepository.FindEnabledByAgentID(db, agent.ID) {
|
||||||
|
definition.WorkflowBindings = append(definition.WorkflowBindings, agentRevisionWorkflowBinding{WorkflowID: binding.WorkflowID, WorkflowVersionID: binding.WorkflowVersionID, ToolName: binding.ToolName, TriggerInstruction: binding.TriggerInstruction, Priority: binding.Priority})
|
||||||
|
}
|
||||||
data, err := json.Marshal(definition)
|
data, err := json.Marshal(definition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -83,11 +83,18 @@ func (s *aIAgentService) CreateAIAgent(req request.CreateAIAgentRequest, operato
|
|||||||
if err := repositories.AIAgentRepository.Create(ctx.Tx, item); err != nil {
|
if err := repositories.AIAgentRepository.Create(ctx.Tx, item); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if item.RuntimeMode == enums.AIAgentRuntimeModeWorkflow || item.RuntimeMode == enums.AIAgentRuntimeModeHybrid {
|
bindings, err := s.replaceWorkflowBindings(ctx.Tx, item.ID, req.WorkflowBindings, operator)
|
||||||
_, err := AIWorkflowService.createDefaultAgentWorkflow(ctx.Tx, item, operator)
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
if len(bindings) == 0 && (item.RuntimeMode == enums.AIAgentRuntimeModeWorkflow || item.RuntimeMode == enums.AIAgentRuntimeModeHybrid) {
|
||||||
|
_, createErr := AIWorkflowService.createDefaultAgentWorkflow(ctx.Tx, item, operator)
|
||||||
|
if createErr != nil {
|
||||||
|
return createErr
|
||||||
|
}
|
||||||
|
return nil // Legacy create calls remain compatible until clients send explicit bindings.
|
||||||
|
}
|
||||||
|
return s.validateWorkflowBindingMode(ctx.Tx, item, bindings)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -139,7 +146,35 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato
|
|||||||
// behavior. The operator must explicitly publish the new revision.
|
// behavior. The operator must explicitly publish the new revision.
|
||||||
columns["published_revision_id"] = 0
|
columns["published_revision_id"] = 0
|
||||||
}
|
}
|
||||||
return repositories.AIAgentRepository.Updates(sqls.DB(), req.ID, columns)
|
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||||
|
if err := repositories.AIAgentRepository.Updates(ctx.Tx, req.ID, columns); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bindings, err := s.replaceWorkflowBindings(ctx.Tx, req.ID, req.WorkflowBindings, operator)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.validateWorkflowBindingMode(ctx.Tx, item, bindings)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *aIAgentService) validateWorkflowBindingMode(db *gorm.DB, agent *models.AIAgent, bindings []models.AIAgentWorkflowBinding) error {
|
||||||
|
enabled := make([]models.AIAgentWorkflowBinding, 0, len(bindings))
|
||||||
|
for _, binding := range bindings {
|
||||||
|
if binding.Enabled {
|
||||||
|
enabled = append(enabled, binding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if agent.RuntimeMode == enums.AIAgentRuntimeModeAutonomous {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(enabled) == 0 {
|
||||||
|
return errorsx.InvalidParam("workflow and hybrid agents require at least one enabled workflow")
|
||||||
|
}
|
||||||
|
if agent.RuntimeMode == enums.AIAgentRuntimeModeWorkflow && len(enabled) != 1 {
|
||||||
|
return errorsx.InvalidParam("workflow agent requires exactly one enabled workflow")
|
||||||
|
}
|
||||||
|
return repositories.AIAgentRepository.Updates(db, agent.ID, map[string]any{"workflow_version_id": enabled[0].WorkflowVersionID})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *aIAgentService) DeleteAIAgent(id int64, operator *dto.AuthPrincipal) error {
|
func (s *aIAgentService) DeleteAIAgent(id int64, operator *dto.AuthPrincipal) error {
|
||||||
@@ -169,12 +204,15 @@ func (s *aIAgentService) PublishAIAgent(id int64, operator *dto.AuthPrincipal) (
|
|||||||
if agent == nil || agent.Status != enums.StatusOk {
|
if agent == nil || agent.Status != enums.StatusOk {
|
||||||
return errorsx.InvalidParamI18n("error.e0002")
|
return errorsx.InvalidParamI18n("error.e0002")
|
||||||
}
|
}
|
||||||
if agent.RuntimeMode == enums.AIAgentRuntimeModeWorkflow || agent.RuntimeMode == enums.AIAgentRuntimeModeHybrid {
|
if agent.RuntimeMode == enums.AIAgentRuntimeModeWorkflow {
|
||||||
return errorsx.InvalidParam("workflow and hybrid agents must publish a workflow version")
|
return errorsx.InvalidParam("workflow agents publish through their selected workflow version")
|
||||||
}
|
}
|
||||||
if err := s.validatePublishableAgent(ctx.Tx, agent); err != nil {
|
if err := s.validatePublishableAgent(ctx.Tx, agent); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if agent.RuntimeMode == enums.AIAgentRuntimeModeHybrid && len(s.ListEnabledWorkflowBindings(ctx.Tx, agent.ID)) == 0 {
|
||||||
|
return errorsx.InvalidParam("hybrid agent requires at least one published workflow")
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
revision, err = AgentRevisionService.PublishSnapshot(ctx.Tx, agent, operator)
|
revision, err = AgentRevisionService.PublishSnapshot(ctx.Tx, agent, operator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"agent-desk/internal/models"
|
||||||
|
"agent-desk/internal/pkg/dto"
|
||||||
|
"agent-desk/internal/pkg/dto/request"
|
||||||
|
"agent-desk/internal/pkg/enums"
|
||||||
|
"agent-desk/internal/pkg/errorsx"
|
||||||
|
"agent-desk/internal/pkg/utils"
|
||||||
|
"agent-desk/internal/repositories"
|
||||||
|
|
||||||
|
"github.com/mlogclub/simple/sqls"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AIAgentWorkflowBindingContext struct {
|
||||||
|
Binding models.AIAgentWorkflowBinding
|
||||||
|
Workflow *models.AIWorkflow
|
||||||
|
Version *models.AIWorkflowVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *aIAgentService) ListWorkflowBindings(agentID int64) []AIAgentWorkflowBindingContext {
|
||||||
|
bindings := repositories.AIAgentWorkflowBindingRepository.FindByAgentID(sqls.DB(), agentID)
|
||||||
|
return s.buildWorkflowBindingContexts(sqls.DB(), bindings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *aIAgentService) ListEnabledWorkflowBindings(db *gorm.DB, agentID int64) []AIAgentWorkflowBindingContext {
|
||||||
|
return s.buildWorkflowBindingContexts(db, repositories.AIAgentWorkflowBindingRepository.FindEnabledByAgentID(db, agentID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *aIAgentService) buildWorkflowBindingContexts(db *gorm.DB, bindings []models.AIAgentWorkflowBinding) []AIAgentWorkflowBindingContext {
|
||||||
|
ret := make([]AIAgentWorkflowBindingContext, 0, len(bindings))
|
||||||
|
for _, binding := range bindings {
|
||||||
|
ret = append(ret, AIAgentWorkflowBindingContext{Binding: binding, Workflow: repositories.AIWorkflowRepository.Get(db, binding.WorkflowID), Version: repositories.AIWorkflowVersionRepository.Get(db, binding.WorkflowVersionID)})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *aIAgentService) replaceWorkflowBindings(db *gorm.DB, agentID int64, input []request.AIAgentWorkflowBindingRequest, operator *dto.AuthPrincipal) ([]models.AIAgentWorkflowBinding, error) {
|
||||||
|
seen := make(map[int64]struct{}, len(input))
|
||||||
|
items := make([]models.AIAgentWorkflowBinding, 0, len(input))
|
||||||
|
for index, item := range input {
|
||||||
|
if item.WorkflowVersionID <= 0 {
|
||||||
|
return nil, errorsx.InvalidParam("workflow version is required")
|
||||||
|
}
|
||||||
|
if _, exists := seen[item.WorkflowVersionID]; exists {
|
||||||
|
return nil, errorsx.InvalidParam("workflow version must not be bound more than once")
|
||||||
|
}
|
||||||
|
seen[item.WorkflowVersionID] = struct{}{}
|
||||||
|
version := repositories.AIWorkflowVersionRepository.Get(db, item.WorkflowVersionID)
|
||||||
|
if version == nil || version.Status != enums.StatusOk {
|
||||||
|
return nil, errorsx.InvalidParam("workflow version is not published")
|
||||||
|
}
|
||||||
|
workflow := repositories.AIWorkflowRepository.Get(db, version.WorkflowID)
|
||||||
|
if workflow == nil || workflow.Status == enums.StatusDeleted {
|
||||||
|
return nil, errorsx.InvalidParam("workflow does not exist")
|
||||||
|
}
|
||||||
|
priority := item.Priority
|
||||||
|
if priority == 0 {
|
||||||
|
priority = index + 1
|
||||||
|
}
|
||||||
|
items = append(items, models.AIAgentWorkflowBinding{AIAgentID: agentID, WorkflowID: version.WorkflowID, WorkflowVersionID: version.ID, ToolName: strings.TrimSpace(item.ToolName), TriggerInstruction: strings.TrimSpace(item.TriggerInstruction), Priority: priority, Enabled: item.Enabled, AuditFields: utils.BuildAuditFields(operator)})
|
||||||
|
}
|
||||||
|
if err := repositories.AIAgentWorkflowBindingRepository.ReplaceByAgentID(db, agentID, items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -463,13 +463,46 @@ func TestAIWorkflowServicePublishAgentWorkflowBindsAgentVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAIAgentServiceBindsPublishedWorkflowVersionIndependently(t *testing.T) {
|
||||||
|
setupAIAgentWorkflowTestDB(t)
|
||||||
|
operator := aiAgentWorkflowTestOperator()
|
||||||
|
workflow, err := AIWorkflowService.CreateWorkflow(request.CreateAIWorkflowRequest{Name: "共享建单流程", Definition: validAIWorkflowDefinition()}, operator)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateWorkflow() error = %v", err)
|
||||||
|
}
|
||||||
|
version, err := AIWorkflowService.PublishWorkflow(request.PublishAIWorkflowRequest{WorkflowID: workflow.ID, Definition: validAIWorkflowDefinition()}, operator)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublishWorkflow() error = %v", err)
|
||||||
|
}
|
||||||
|
agent, err := AIAgentService.CreateAIAgent(request.CreateAIAgentRequest{
|
||||||
|
Name: "绑定共享工作流的 Agent", AIConfigID: createAIAgentWorkflowTestConfig(t), RuntimeMode: enums.AIAgentRuntimeModeHybrid,
|
||||||
|
ServiceMode: enums.IMConversationServiceModeAIOnly, HandoffMode: enums.AIAgentHandoffModeWaitPool, FallbackMode: enums.AIAgentFallbackModeNoAnswer,
|
||||||
|
WorkflowBindings: []request.AIAgentWorkflowBindingRequest{{WorkflowVersionID: version.ID, ToolName: "创建工单", TriggerInstruction: "用户要求创建工单", Enabled: true}},
|
||||||
|
}, operator)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateAIAgent() error = %v", err)
|
||||||
|
}
|
||||||
|
bindings := AIAgentService.ListWorkflowBindings(agent.ID)
|
||||||
|
if len(bindings) != 1 || bindings[0].Binding.WorkflowVersionID != version.ID || bindings[0].Workflow == nil || bindings[0].Workflow.AgentID != 0 {
|
||||||
|
t.Fatalf("unexpected independent workflow binding: %#v", bindings)
|
||||||
|
}
|
||||||
|
if _, err := AIAgentService.PublishAIAgent(agent.ID, operator); err != nil {
|
||||||
|
t.Fatalf("PublishAIAgent() error = %v", err)
|
||||||
|
}
|
||||||
|
stored := AIAgentService.Get(agent.ID)
|
||||||
|
snapshot, err := AgentRevisionService.ResolvePublishedSnapshot(*stored, *AIConfigService.Get(stored.AIConfigID))
|
||||||
|
if err != nil || len(snapshot.WorkflowBindings) != 1 || snapshot.WorkflowBindings[0].WorkflowVersionID != version.ID {
|
||||||
|
t.Fatalf("expected published workflow binding snapshot, snapshot=%#v err=%v", snapshot, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setupAIAgentWorkflowTestDB(t *testing.T) {
|
func setupAIAgentWorkflowTestDB(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open sqlite db: %v", err)
|
t.Fatalf("open sqlite db: %v", err)
|
||||||
}
|
}
|
||||||
if err := db.AutoMigrate(&models.AIAgent{}, &models.AIConfig{}, &models.KnowledgeBase{}, &models.AIWorkflow{}, &models.AIWorkflowVersion{}, &models.AgentRevision{}); err != nil {
|
if err := db.AutoMigrate(&models.AIAgent{}, &models.AIConfig{}, &models.KnowledgeBase{}, &models.AIWorkflow{}, &models.AIWorkflowVersion{}, &models.AIAgentWorkflowBinding{}, &models.AgentRevision{}); err != nil {
|
||||||
t.Fatalf("auto migrate: %v", err)
|
t.Fatalf("auto migrate: %v", err)
|
||||||
}
|
}
|
||||||
sqls.SetDB(db)
|
sqls.SetDB(db)
|
||||||
|
|||||||
@@ -246,7 +246,22 @@ func (s *aiWorkflowService) ValidateDefinition(def dsl.Definition) workflowvalid
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *aiWorkflowService) CreateWorkflow(req request.CreateAIWorkflowRequest, operator *dto.AuthPrincipal) (*models.AIWorkflow, error) {
|
func (s *aiWorkflowService) CreateWorkflow(req request.CreateAIWorkflowRequest, operator *dto.AuthPrincipal) (*models.AIWorkflow, error) {
|
||||||
return s.SaveAgentWorkflow(req, operator)
|
if operator == nil {
|
||||||
|
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(req.Name)
|
||||||
|
if name == "" {
|
||||||
|
return nil, errorsx.InvalidParam("workflow name is required")
|
||||||
|
}
|
||||||
|
definition, err := marshalDefinition(req.Definition)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item := &models.AIWorkflow{Name: name, Description: strings.TrimSpace(req.Description), Status: enums.StatusOk, DraftDefinition: definition, AuditFields: utils.BuildAuditFields(operator)}
|
||||||
|
if err := repositories.AIWorkflowRepository.Create(sqls.DB(), item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *aiWorkflowService) SaveAgentWorkflow(req request.SaveAIWorkflowRequest, operator *dto.AuthPrincipal) (*models.AIWorkflow, error) {
|
func (s *aiWorkflowService) SaveAgentWorkflow(req request.SaveAIWorkflowRequest, operator *dto.AuthPrincipal) (*models.AIWorkflow, error) {
|
||||||
@@ -305,9 +320,6 @@ func (s *aiWorkflowService) UpdateWorkflow(req request.UpdateAIWorkflowRequest,
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
return errorsx.InvalidParam("workflow name is required")
|
return errorsx.InvalidParam("workflow name is required")
|
||||||
}
|
}
|
||||||
if req.AgentID <= 0 {
|
|
||||||
return errorsx.InvalidParam("agent id is required")
|
|
||||||
}
|
|
||||||
definition, err := marshalDefinition(req.Definition)
|
definition, err := marshalDefinition(req.Definition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -315,7 +327,6 @@ func (s *aiWorkflowService) UpdateWorkflow(req request.UpdateAIWorkflowRequest,
|
|||||||
return repositories.AIWorkflowRepository.Updates(sqls.DB(), req.ID, map[string]interface{}{
|
return repositories.AIWorkflowRepository.Updates(sqls.DB(), req.ID, map[string]interface{}{
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": strings.TrimSpace(req.Description),
|
"description": strings.TrimSpace(req.Description),
|
||||||
"agent_id": req.AgentID,
|
|
||||||
"draft_definition": definition,
|
"draft_definition": definition,
|
||||||
"update_user_id": operator.UserID,
|
"update_user_id": operator.UserID,
|
||||||
"update_user_name": operator.Username,
|
"update_user_name": operator.Username,
|
||||||
@@ -330,6 +341,9 @@ func (s *aiWorkflowService) DeleteWorkflow(id int64, operator *dto.AuthPrincipal
|
|||||||
if s.Get(id) == nil {
|
if s.Get(id) == nil {
|
||||||
return errorsx.InvalidParamI18n("error.e0002")
|
return errorsx.InvalidParamI18n("error.e0002")
|
||||||
}
|
}
|
||||||
|
if repositories.AIAgentWorkflowBindingRepository.CountByWorkflowID(sqls.DB(), id) > 0 {
|
||||||
|
return errorsx.InvalidParam("workflow is still associated with an agent")
|
||||||
|
}
|
||||||
return repositories.AIWorkflowRepository.Updates(sqls.DB(), id, map[string]interface{}{
|
return repositories.AIWorkflowRepository.Updates(sqls.DB(), id, map[string]interface{}{
|
||||||
"status": enums.StatusDeleted,
|
"status": enums.StatusDeleted,
|
||||||
"update_user_id": operator.UserID,
|
"update_user_id": operator.UserID,
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ func setupAIWorkflowTestDB(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open sqlite db: %v", err)
|
t.Fatalf("open sqlite db: %v", err)
|
||||||
}
|
}
|
||||||
if err := db.AutoMigrate(&models.AIAgent{}, &models.AIWorkflow{}, &models.AIWorkflowVersion{}, &models.AIWorkflowRun{}, &models.AIWorkflowNodeRun{}); err != nil {
|
if err := db.AutoMigrate(&models.AIAgent{}, &models.AIWorkflow{}, &models.AIWorkflowVersion{}, &models.AIAgentWorkflowBinding{}, &models.AIWorkflowRun{}, &models.AIWorkflowNodeRun{}); err != nil {
|
||||||
t.Fatalf("auto migrate: %v", err)
|
t.Fatalf("auto migrate: %v", err)
|
||||||
}
|
}
|
||||||
sqls.SetDB(db)
|
sqls.SetDB(db)
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func setupChannelServiceTestDB(t *testing.T) *gorm.DB {
|
|||||||
_ = sqlDB.Close()
|
_ = sqlDB.Close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err := db.AutoMigrate(&models.AIAgent{}, &models.AgentRevision{}, &models.Channel{}); err != nil {
|
if err := db.AutoMigrate(&models.AIAgent{}, &models.AgentRevision{}, &models.AIAgentWorkflowBinding{}, &models.Channel{}); err != nil {
|
||||||
t.Fatalf("auto migrate: %v", err)
|
t.Fatalf("auto migrate: %v", err)
|
||||||
}
|
}
|
||||||
sqls.SetDB(db)
|
sqls.SetDB(db)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
fetchAIWorkflowNodeSpecs,
|
fetchAIWorkflowNodeSpecs,
|
||||||
fetchAIWorkflowTemplates,
|
fetchAIWorkflowTemplates,
|
||||||
fetchAIWorkflowVersions,
|
fetchAIWorkflowVersions,
|
||||||
|
fetchAIWorkflows,
|
||||||
fetchAgentTeamsAll,
|
fetchAgentTeamsAll,
|
||||||
fetchMCPCatalog,
|
fetchMCPCatalog,
|
||||||
fetchSkillDefinitionsAll,
|
fetchSkillDefinitionsAll,
|
||||||
@@ -56,9 +57,11 @@ import {
|
|||||||
updateAIAgent,
|
updateAIAgent,
|
||||||
validateAIWorkflow,
|
validateAIWorkflow,
|
||||||
type AIAgent,
|
type AIAgent,
|
||||||
|
type AIAgentWorkflowBindingInput,
|
||||||
type AgentRevision,
|
type AgentRevision,
|
||||||
type AIConfig,
|
type AIConfig,
|
||||||
type AIWorkflowDefinition,
|
type AIWorkflowDefinition,
|
||||||
|
type AIWorkflow,
|
||||||
type AIWorkflowNodeSpec,
|
type AIWorkflowNodeSpec,
|
||||||
type AIWorkflowTemplate,
|
type AIWorkflowTemplate,
|
||||||
type AIWorkflowVersion,
|
type AIWorkflowVersion,
|
||||||
@@ -161,6 +164,9 @@ export function AIAgentConfigWorkbench({
|
|||||||
const [selectedSkillIds, setSelectedSkillIds] = useState<number[]>([])
|
const [selectedSkillIds, setSelectedSkillIds] = useState<number[]>([])
|
||||||
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<number[]>([])
|
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<number[]>([])
|
||||||
const [directTools, setDirectTools] = useState<DirectToolItem[]>([])
|
const [directTools, setDirectTools] = useState<DirectToolItem[]>([])
|
||||||
|
const [workflowBindings, setWorkflowBindings] = useState<AIAgentWorkflowBindingInput[]>([])
|
||||||
|
const [publishedWorkflows, setPublishedWorkflows] = useState<AIWorkflow[]>([])
|
||||||
|
const [workflowToAdd, setWorkflowToAdd] = useState("")
|
||||||
|
|
||||||
const [definition, setDefinition] = useState<AIWorkflowDefinition>(fallbackDefinition)
|
const [definition, setDefinition] = useState<AIWorkflowDefinition>(fallbackDefinition)
|
||||||
const [workflowRevision, setWorkflowRevision] = useState(0)
|
const [workflowRevision, setWorkflowRevision] = useState(0)
|
||||||
@@ -200,6 +206,7 @@ export function AIAgentConfigWorkbench({
|
|||||||
skillList,
|
skillList,
|
||||||
knowledgeBaseList,
|
knowledgeBaseList,
|
||||||
catalog,
|
catalog,
|
||||||
|
workflowPage,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchAIWorkflowNodeSpecs(),
|
fetchAIWorkflowNodeSpecs(),
|
||||||
fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition),
|
fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition),
|
||||||
@@ -209,6 +216,7 @@ export function AIAgentConfigWorkbench({
|
|||||||
fetchSkillDefinitionsAll({ status: Status.Ok }),
|
fetchSkillDefinitionsAll({ status: Status.Ok }),
|
||||||
fetchKnowledgeBasesAll({ status: Status.Ok }),
|
fetchKnowledgeBasesAll({ status: Status.Ok }),
|
||||||
fetchMCPCatalog(),
|
fetchMCPCatalog(),
|
||||||
|
fetchAIWorkflows({ limit: 100 }),
|
||||||
])
|
])
|
||||||
|
|
||||||
setNodeSpecs(specs ?? [])
|
setNodeSpecs(specs ?? [])
|
||||||
@@ -218,6 +226,7 @@ export function AIAgentConfigWorkbench({
|
|||||||
setSkills(skillList ?? [])
|
setSkills(skillList ?? [])
|
||||||
setKnowledgeBases(knowledgeBaseList ?? [])
|
setKnowledgeBases(knowledgeBaseList ?? [])
|
||||||
setToolCatalog(catalog ?? [])
|
setToolCatalog(catalog ?? [])
|
||||||
|
setPublishedWorkflows((workflowPage.results ?? []).filter((item) => item.publishedVersionId > 0))
|
||||||
|
|
||||||
if (!currentAgentId || currentAgentId <= 0) {
|
if (!currentAgentId || currentAgentId <= 0) {
|
||||||
setAgent(null)
|
setAgent(null)
|
||||||
@@ -239,24 +248,19 @@ export function AIAgentConfigWorkbench({
|
|||||||
setSelectedSkillIds([])
|
setSelectedSkillIds([])
|
||||||
setSelectedKnowledgeBaseIds([])
|
setSelectedKnowledgeBaseIds([])
|
||||||
setDirectTools([])
|
setDirectTools([])
|
||||||
|
setWorkflowBindings([])
|
||||||
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
|
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const [agentDetail, workflowDetail, revisionList] = await Promise.all([
|
const [agentDetail, revisionList] = await Promise.all([
|
||||||
fetchAIAgent(currentAgentId),
|
fetchAIAgent(currentAgentId),
|
||||||
fetchAIAgentWorkflow(currentAgentId),
|
|
||||||
fetchAIAgentRevisions(currentAgentId),
|
fetchAIAgentRevisions(currentAgentId),
|
||||||
])
|
])
|
||||||
|
|
||||||
setAgent(agentDetail)
|
setAgent(agentDetail)
|
||||||
setAgentRevisions(revisionList ?? [])
|
setAgentRevisions(revisionList ?? [])
|
||||||
if (workflowDetail?.id > 0) {
|
setWorkflowVersions([])
|
||||||
const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 })
|
|
||||||
setWorkflowVersions(versionPage.results ?? [])
|
|
||||||
} else {
|
|
||||||
setWorkflowVersions([])
|
|
||||||
}
|
|
||||||
setName(agentDetail.name)
|
setName(agentDetail.name)
|
||||||
setDescription(agentDetail.description || "")
|
setDescription(agentDetail.description || "")
|
||||||
setAIConfigId(toText(agentDetail.aiConfigId))
|
setAIConfigId(toText(agentDetail.aiConfigId))
|
||||||
@@ -273,7 +277,8 @@ export function AIAgentConfigWorkbench({
|
|||||||
setSelectedSkillIds(agentDetail.skillIds ?? [])
|
setSelectedSkillIds(agentDetail.skillIds ?? [])
|
||||||
setSelectedKnowledgeBaseIds(agentDetail.knowledgeBaseIds ?? [])
|
setSelectedKnowledgeBaseIds(agentDetail.knowledgeBaseIds ?? [])
|
||||||
setDirectTools(agentDetail.directTools ?? [])
|
setDirectTools(agentDetail.directTools ?? [])
|
||||||
replaceWorkflowDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition)
|
setWorkflowBindings((agentDetail.workflowBindings ?? []).map(({ workflowVersionId, toolName, triggerInstruction, priority, enabled }) => ({ workflowVersionId, toolName, triggerInstruction, priority, enabled })))
|
||||||
|
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to load Agent config")
|
toast.error(error instanceof Error ? error.message : "Failed to load Agent config")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -296,8 +301,8 @@ export function AIAgentConfigWorkbench({
|
|||||||
const runtimeModeOptions = useMemo(
|
const runtimeModeOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: "autonomous", label: "自主接待" },
|
{ value: "autonomous", label: "自主接待" },
|
||||||
{ value: "hybrid", label: "自主接待 + 流程" },
|
{ value: "hybrid", label: "自主接待 + 工作流" },
|
||||||
{ value: "workflow", label: "高级编排 / Playbooks" },
|
{ value: "workflow", label: "仅工作流" },
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -402,6 +407,13 @@ export function AIAgentConfigWorkbench({
|
|||||||
setDirectToolToAdd("")
|
setDirectToolToAdd("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addWorkflowBinding(value: string) {
|
||||||
|
const workflow = publishedWorkflows.find((item) => item.publishedVersionId === Number(value))
|
||||||
|
if (!workflow || workflowBindings.some((item) => item.workflowVersionId === workflow.publishedVersionId)) return
|
||||||
|
setWorkflowBindings((current) => [...current, { workflowVersionId: workflow.publishedVersionId, toolName: workflow.name, triggerInstruction: "", priority: current.length + 1, enabled: true }])
|
||||||
|
setWorkflowToAdd("")
|
||||||
|
}
|
||||||
|
|
||||||
function buildPayload(): CreateAIAgentPayload {
|
function buildPayload(): CreateAIAgentPayload {
|
||||||
return {
|
return {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
@@ -419,7 +431,8 @@ export function AIAgentConfigWorkbench({
|
|||||||
fallbackMessage: fallbackMessage.trim(),
|
fallbackMessage: fallbackMessage.trim(),
|
||||||
knowledgeBaseIds: uniqueNumbers(selectedKnowledgeBaseIds),
|
knowledgeBaseIds: uniqueNumbers(selectedKnowledgeBaseIds),
|
||||||
skillIds: uniqueNumbers(selectedSkillIds),
|
skillIds: uniqueNumbers(selectedSkillIds),
|
||||||
directTools,
|
directTools,
|
||||||
|
workflowBindings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,12 +460,12 @@ export function AIAgentConfigWorkbench({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function publishAutonomousAgent() {
|
async function publishAutonomousAgent() {
|
||||||
if (!agent || runtimeMode !== "autonomous") return
|
if (!agent || (runtimeMode !== "autonomous" && runtimeMode !== "hybrid")) return
|
||||||
setSavingAgent(true)
|
setSavingAgent(true)
|
||||||
try {
|
try {
|
||||||
await publishAIAgent(agent.id)
|
await publishAIAgent(agent.id)
|
||||||
await loadData()
|
await loadData()
|
||||||
toast.success("Autonomous Agent published")
|
toast.success("Agent published")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to publish Autonomous Agent")
|
toast.error(error instanceof Error ? error.message : "Failed to publish Autonomous Agent")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -580,7 +593,7 @@ export function AIAgentConfigWorkbench({
|
|||||||
const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [
|
const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [
|
||||||
{ key: "basic", title: "基础信息", icon: <SettingsIcon /> },
|
{ key: "basic", title: "基础信息", icon: <SettingsIcon /> },
|
||||||
{ key: "capabilities", title: "能力来源", icon: <PlugIcon /> },
|
{ key: "capabilities", title: "能力来源", icon: <PlugIcon /> },
|
||||||
{ key: "workflow", title: "高级编排 / Playbooks", icon: <GitBranchIcon /> },
|
{ key: "workflow", title: "关联工作流", icon: <GitBranchIcon /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions)
|
const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions)
|
||||||
@@ -615,7 +628,7 @@ export function AIAgentConfigWorkbench({
|
|||||||
null
|
null
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{agent && runtimeMode === "autonomous" ? <Button type="button" variant="outline" disabled={savingAgent || loading} onClick={publishAutonomousAgent}>发布 Agent</Button> : null}
|
{agent && (runtimeMode === "autonomous" || runtimeMode === "hybrid") ? <Button type="button" variant="outline" disabled={savingAgent || loading} onClick={publishAutonomousAgent}>发布 Agent</Button> : null}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -633,7 +646,7 @@ export function AIAgentConfigWorkbench({
|
|||||||
<div className="flex min-h-0 flex-1 flex-col bg-background">
|
<div className="flex min-h-0 flex-1 flex-col bg-background">
|
||||||
{agent && !runtimePublished ? (
|
{agent && !runtimePublished ? (
|
||||||
<div className="shrink-0 border-b border-amber-200 bg-amber-50 px-5 py-2 text-sm text-amber-900">
|
<div className="shrink-0 border-b border-amber-200 bg-amber-50 px-5 py-2 text-sm text-amber-900">
|
||||||
{runtimeMode === "autonomous" ? "未发布 Agent,AI 不会自动回复。保存配置后发布 Agent,再绑定渠道或启用自动回复。" : runtimeMode === "hybrid" ? "未发布 Hybrid Agent,AI 不会自动回复。保存配置后请进入“高级编排 / Playbooks”发布一个版本。" : "未发布 Playbook,AI 不会自动回复。保存配置后请进入“高级编排 / Playbooks”发布一个版本,再绑定渠道或启用自动回复。"}
|
{runtimeMode === "autonomous" ? "未发布 Agent,AI 不会自动回复。保存配置后发布 Agent,再绑定渠道或启用自动回复。" : runtimeMode === "hybrid" ? "未发布 Hybrid Agent,AI 不会自动回复。保存配置后请关联已发布工作流,再发布 Agent。" : "未发布工作流,AI 不会自动回复。请先关联一个已发布工作流。"}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="shrink-0 border-b bg-muted/30 px-4 py-2">
|
<div className="shrink-0 border-b bg-muted/30 px-4 py-2">
|
||||||
@@ -870,46 +883,23 @@ export function AIAgentConfigWorkbench({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{activeSection === "workflow" ? (
|
{activeSection === "workflow" ? (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<ConfigSection>
|
||||||
<div className="flex shrink-0 items-center gap-2 border-b px-4 py-2">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<OptionCombobox
|
<div><h2 className="text-base font-semibold">关联工作流</h2><p className="mt-1 text-sm text-muted-foreground">仅可关联已发布版本。工作流内容在“工作流”中心独立维护,保存 Agent 草稿后才会更新关联。</p></div>
|
||||||
value={selectedWorkflowTemplate}
|
<Button type="button" variant="outline" onClick={() => window.location.assign("/dashboard/ai-workflows")}>管理工作流</Button>
|
||||||
options={workflowTemplates.map((item) => ({ value: item.code, label: item.name }))}
|
|
||||||
placeholder="选择 Playbook 模板"
|
|
||||||
onChange={setSelectedWorkflowTemplate}
|
|
||||||
/>
|
|
||||||
<Button type="button" variant="outline" size="sm" disabled={!selectedWorkflowTemplate || savingWorkflow || loading} onClick={applySelectedWorkflowTemplate}>
|
|
||||||
应用模板
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<WorkflowEditor
|
<div className="flex max-w-xl items-center gap-2">
|
||||||
key={workflowRevision}
|
<OptionCombobox value={workflowToAdd} options={publishedWorkflows.filter((item) => !workflowBindings.some((binding) => binding.workflowVersionId === item.publishedVersionId)).map((item) => ({ value: String(item.publishedVersionId), label: `${item.name} · v#${item.publishedVersionId}` }))} placeholder="选择已发布工作流" onChange={setWorkflowToAdd} />
|
||||||
definition={definition}
|
<Button type="button" variant="outline" disabled={!workflowToAdd} onClick={() => addWorkflowBinding(workflowToAdd)}>关联</Button>
|
||||||
nodeSpecs={nodeSpecs}
|
</div>
|
||||||
onDefinitionChange={setDefinition}
|
<div className="space-y-2">
|
||||||
historyDisabled={savingWorkflow || loading}
|
{workflowBindings.length === 0 ? <div className="rounded-md border border-dashed p-5 text-sm text-muted-foreground">尚未关联工作流。自主接待无需配置;Hybrid 至少需要关联一个已发布工作流。</div> : workflowBindings.map((binding) => {
|
||||||
onRestoreDefault={restoreDefaultWorkflow}
|
const workflow = publishedWorkflows.find((item) => item.publishedVersionId === binding.workflowVersionId)
|
||||||
restoreDefaultDisabled={savingWorkflow || loading}
|
return <div key={binding.workflowVersionId} className="flex items-center gap-3 rounded-md border p-3"><GitBranchIcon className="size-4 text-muted-foreground" /><div className="min-w-0 flex-1"><div className="font-medium">{workflow?.name || binding.toolName || `工作流版本 #${binding.workflowVersionId}`}</div><div className="text-xs text-muted-foreground">固定版本 #{binding.workflowVersionId}</div></div><Button type="button" variant="ghost" size="sm" onClick={() => setWorkflowBindings((current) => current.filter((item) => item.workflowVersionId !== binding.workflowVersionId))}>移除</Button></div>
|
||||||
onValidate={validateWorkflowDraft}
|
})}
|
||||||
validateDisabled={savingWorkflow || loading || !currentAgentId}
|
</div>
|
||||||
onSaveDraft={saveWorkflowDraft}
|
<div className="flex justify-end gap-2"><Button type="button" disabled={savingAgent || loading} onClick={saveAgentSettings}>保存 Agent 配置</Button>{agent && runtimeMode === "hybrid" ? <Button type="button" disabled={savingAgent || loading} onClick={publishAutonomousAgent}>发布 Agent</Button> : null}</div>
|
||||||
saveDraftDisabled={savingWorkflow || loading || !currentAgentId}
|
</ConfigSection>
|
||||||
onPublish={publishWorkflow}
|
|
||||||
publishDisabled={savingWorkflow || loading || !currentAgentId}
|
|
||||||
toolbarExtra={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
||||||
onClick={() => setVersionDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<HistoryIcon className="size-3.5" />
|
|
||||||
版本记录
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Dialog open={versionDialogOpen} onOpenChange={setVersionDialogOpen}>
|
<Dialog open={versionDialogOpen} onOpenChange={setVersionDialogOpen}>
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default function DashboardAIAgentsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "workflow",
|
key: "workflow",
|
||||||
label: "Playbook 状态",
|
label: "工作流状态",
|
||||||
render: (item) => {
|
render: (item) => {
|
||||||
const published = isWorkflowPublished(item);
|
const published = isWorkflowPublished(item);
|
||||||
return (
|
return (
|
||||||
@@ -144,7 +144,7 @@ export default function DashboardAIAgentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
{!published ? (
|
{!published ? (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
未发布 Playbook,AI 不会自动回复
|
未发布工作流,AI 不会自动回复
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { PlusIcon } from "lucide-react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import {
|
||||||
|
createAIWorkflow,
|
||||||
|
fetchAIWorkflow,
|
||||||
|
fetchAIWorkflowNodeSpecs,
|
||||||
|
fetchAIWorkflows,
|
||||||
|
publishAIWorkflow,
|
||||||
|
updateAIWorkflow,
|
||||||
|
validateAIWorkflow,
|
||||||
|
type AIWorkflow,
|
||||||
|
type AIWorkflowDefinition,
|
||||||
|
type AIWorkflowNodeSpec,
|
||||||
|
} from "@/lib/api/admin"
|
||||||
|
|
||||||
|
import { WorkflowEditor } from "./_components/workflow-editor"
|
||||||
|
|
||||||
|
const emptyDefinition: AIWorkflowDefinition = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
nodes: [
|
||||||
|
{ id: "start_1", type: "start", meta: { position: { x: 0, y: 80 } }, data: { title: "开始", config: {}, inputsValues: {} } },
|
||||||
|
{ id: "end_1", type: "end", meta: { position: { x: 260, y: 80 } }, data: { title: "结束", config: {}, inputsValues: {} } },
|
||||||
|
],
|
||||||
|
edges: [{ sourceNodeID: "start_1", targetNodeID: "end_1", sourcePortID: "edge_start_end" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardAIWorkflowsPage() {
|
||||||
|
const [items, setItems] = useState<AIWorkflow[]>([])
|
||||||
|
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
|
||||||
|
const [active, setActive] = useState<AIWorkflow | null>(null)
|
||||||
|
const [name, setName] = useState("")
|
||||||
|
const [description, setDescription] = useState("")
|
||||||
|
const [definition, setDefinition] = useState<AIWorkflowDefinition>(emptyDefinition)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
const select = useCallback(async (id: number) => {
|
||||||
|
const item = await fetchAIWorkflow(id)
|
||||||
|
setActive(item)
|
||||||
|
setName(item.name)
|
||||||
|
setDescription(item.description)
|
||||||
|
setDefinition(item.draftDefinition)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
const [page, specs] = await Promise.all([fetchAIWorkflows({ limit: 100 }), fetchAIWorkflowNodeSpecs()])
|
||||||
|
setItems(page.results)
|
||||||
|
setNodeSpecs(specs)
|
||||||
|
if (!active && page.results[0]) await select(page.results[0].id)
|
||||||
|
}, [active, select])
|
||||||
|
|
||||||
|
useEffect(() => { void reload().catch((error) => toast.error(error instanceof Error ? error.message : "加载工作流失败")) }, [reload])
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!name.trim()) { toast.error("请填写工作流名称"); return }
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
if (active) {
|
||||||
|
await updateAIWorkflow({ id: active.id, name: name.trim(), description: description.trim(), definition })
|
||||||
|
await select(active.id)
|
||||||
|
} else {
|
||||||
|
const created = await createAIWorkflow({ name: name.trim(), description: description.trim(), definition })
|
||||||
|
await select(created.id)
|
||||||
|
}
|
||||||
|
await reload()
|
||||||
|
toast.success("工作流草稿已保存")
|
||||||
|
} catch (error) { toast.error(error instanceof Error ? error.message : "保存工作流失败") } finally { setSaving(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publish() {
|
||||||
|
if (!active) { toast.error("请先保存工作流草稿"); return }
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const version = await publishAIWorkflow(active.id, definition)
|
||||||
|
await select(active.id)
|
||||||
|
await reload()
|
||||||
|
toast.success(`已发布工作流 v${version.version}`)
|
||||||
|
} catch (error) { toast.error(error instanceof Error ? error.message : "发布工作流失败") } finally { setSaving(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function create() { setActive(null); setName(""); setDescription(""); setDefinition(emptyDefinition) }
|
||||||
|
|
||||||
|
return <div className="flex h-full min-h-0 bg-background">
|
||||||
|
<aside className="w-72 shrink-0 border-r bg-muted/20 p-3">
|
||||||
|
<div className="mb-3 flex items-center justify-between"><div><h1 className="font-semibold">工作流</h1><p className="text-xs text-muted-foreground">独立维护、发布后供 Agent 关联</p></div><Button size="icon" variant="outline" onClick={create}><PlusIcon className="size-4" /></Button></div>
|
||||||
|
<div className="space-y-1">{items.map((item) => <button key={item.id} type="button" onClick={() => void select(item.id)} className={`w-full rounded-md p-3 text-left ${active?.id === item.id ? "bg-background shadow-sm" : "hover:bg-background/70"}`}><div className="truncate font-medium">{item.name}</div><div className="mt-1 text-xs text-muted-foreground">{item.publishedVersionId > 0 ? `已发布版本 #${item.publishedVersionId}` : "未发布"}</div></button>)}</div>
|
||||||
|
</aside>
|
||||||
|
<section className="flex min-w-0 flex-1 flex-col">
|
||||||
|
<header className="flex shrink-0 items-center gap-3 border-b px-5 py-3"><div className="min-w-0 flex-1"><Input value={name} onChange={(event) => setName(event.target.value)} placeholder="工作流名称" className="max-w-sm" /><Textarea value={description} onChange={(event) => setDescription(event.target.value)} placeholder="业务说明(可选)" className="mt-2 min-h-16 max-w-xl resize-none" /></div><Button variant="outline" disabled={saving} onClick={() => void save()}>保存草稿</Button><Button disabled={saving || !active} onClick={() => void publish()}>发布工作流</Button></header>
|
||||||
|
<div className="min-h-0 flex-1"><WorkflowEditor definition={definition} nodeSpecs={nodeSpecs} onDefinitionChange={setDefinition} onValidate={() => void validateAIWorkflow(definition).then((result) => toast[result.valid ? "success" : "error"](result.valid ? "工作流校验通过" : "工作流存在校验错误"))} validateDisabled={saving} /></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
+54
-3
@@ -241,7 +241,7 @@ export type AIAgent = {
|
|||||||
knowledgeBaseIds: number[]
|
knowledgeBaseIds: number[]
|
||||||
skillIds: number[]
|
skillIds: number[]
|
||||||
skills: { id: number; name: string }[]
|
skills: { id: number; name: string }[]
|
||||||
directTools: {
|
directTools: {
|
||||||
toolCode: string
|
toolCode: string
|
||||||
serverCode: string
|
serverCode: string
|
||||||
toolName: string
|
toolName: string
|
||||||
@@ -249,6 +249,7 @@ export type AIAgent = {
|
|||||||
description: string
|
description: string
|
||||||
arguments?: Record<string, string>
|
arguments?: Record<string, string>
|
||||||
}[]
|
}[]
|
||||||
|
workflowBindings: AIAgentWorkflowBinding[]
|
||||||
workflowVersionId: number
|
workflowVersionId: number
|
||||||
publishedRevisionId: number
|
publishedRevisionId: number
|
||||||
workflowPublished: boolean
|
workflowPublished: boolean
|
||||||
@@ -289,6 +290,27 @@ export type CreateAIAgentPayload = {
|
|||||||
description: string
|
description: string
|
||||||
arguments?: Record<string, string>
|
arguments?: Record<string, string>
|
||||||
}[]
|
}[]
|
||||||
|
workflowBindings: AIAgentWorkflowBindingInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIAgentWorkflowBinding = {
|
||||||
|
id: number
|
||||||
|
workflowId: number
|
||||||
|
workflowVersionId: number
|
||||||
|
workflowName: string
|
||||||
|
workflowVersion: number
|
||||||
|
toolName: string
|
||||||
|
triggerInstruction: string
|
||||||
|
priority: number
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIAgentWorkflowBindingInput = {
|
||||||
|
workflowVersionId: number
|
||||||
|
toolName: string
|
||||||
|
triggerInstruction: string
|
||||||
|
priority: number
|
||||||
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateAIAgentPayload = CreateAIAgentPayload & {
|
export type UpdateAIAgentPayload = CreateAIAgentPayload & {
|
||||||
@@ -437,10 +459,12 @@ export type AIWorkflowValidationResult = {
|
|||||||
export type CreateAIWorkflowPayload = {
|
export type CreateAIWorkflowPayload = {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
agentId: number
|
agentId?: number
|
||||||
definition: AIWorkflowDefinition
|
definition: AIWorkflowDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateAIWorkflowPayload = CreateAIWorkflowPayload & { id: number }
|
||||||
|
|
||||||
export type CreateAdminQuickReplyPayload = {
|
export type CreateAdminQuickReplyPayload = {
|
||||||
groupName: string
|
groupName: string
|
||||||
title: string
|
title: string
|
||||||
@@ -1013,6 +1037,26 @@ export function fetchAIAgentWorkflow(agentId: number) {
|
|||||||
return request<AIWorkflow>(`/api/dashboard/ai-agent/${agentId}/workflow`)
|
return request<AIWorkflow>(`/api/dashboard/ai-agent/${agentId}/workflow`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAIWorkflows(query?: Record<string, string | number | undefined>) {
|
||||||
|
return request<PageResult<AIWorkflow>>(`/api/dashboard/ai-workflow/list${toQueryString(query)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAIWorkflow(id: number) {
|
||||||
|
return request<AIWorkflow>(`/api/dashboard/ai-workflow/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAIWorkflow(payload: CreateAIWorkflowPayload) {
|
||||||
|
return request<AIWorkflow>("/api/dashboard/ai-workflow/create", { method: "POST", body: JSON.stringify(payload) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAIWorkflow(payload: UpdateAIWorkflowPayload) {
|
||||||
|
return request<void>("/api/dashboard/ai-workflow/update", { method: "POST", body: JSON.stringify(payload) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAIWorkflow(id: number) {
|
||||||
|
return request<void>("/api/dashboard/ai-workflow/delete", { method: "POST", body: JSON.stringify({ id }) })
|
||||||
|
}
|
||||||
|
|
||||||
export function saveAIAgentWorkflow(payload: CreateAIWorkflowPayload) {
|
export function saveAIAgentWorkflow(payload: CreateAIWorkflowPayload) {
|
||||||
return request<AIWorkflow>("/api/dashboard/ai-agent/workflow/save", {
|
return request<AIWorkflow>("/api/dashboard/ai-agent/workflow/save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -1039,12 +1083,19 @@ export function fetchAIWorkflowVersions(query?: Record<string, string | number |
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateAIWorkflow(definition: AIWorkflowDefinition) {
|
export function validateAIWorkflow(definition: AIWorkflowDefinition) {
|
||||||
return request<AIWorkflowValidationResult>("/api/dashboard/ai-agent/workflow/validate", {
|
return request<AIWorkflowValidationResult>("/api/dashboard/ai-workflow/validate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ definition }),
|
body: JSON.stringify({ definition }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function publishAIWorkflow(workflowId: number, definition: AIWorkflowDefinition) {
|
||||||
|
return request<AIWorkflowVersion>("/api/dashboard/ai-workflow/publish", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ workflowId, definition }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function publishAIAgentWorkflow(agentId: number, definition: AIWorkflowDefinition) {
|
export function publishAIAgentWorkflow(agentId: number, definition: AIWorkflowDefinition) {
|
||||||
return request<AIWorkflowVersion>("/api/dashboard/ai-agent/workflow/publish", {
|
return request<AIWorkflowVersion>("/api/dashboard/ai-agent/workflow/publish", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -193,6 +193,12 @@ export const dashboardNavSections: DashboardNavSectionConfig[] = [
|
|||||||
icon: <MessageSquareMoreIcon />,
|
icon: <MessageSquareMoreIcon />,
|
||||||
requiredPermission: "aiAgent.view",
|
requiredPermission: "aiAgent.view",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
titleKey: "nav.workflows",
|
||||||
|
url: "/dashboard/ai-workflows",
|
||||||
|
icon: <WorkflowIcon />,
|
||||||
|
requiredPermission: "aiAgent.view",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
titleKey: "nav.skillDefinition",
|
titleKey: "nav.skillDefinition",
|
||||||
url: "/dashboard/skill-definition",
|
url: "/dashboard/skill-definition",
|
||||||
|
|||||||
@@ -2340,6 +2340,7 @@
|
|||||||
"aiAgents": "Agents",
|
"aiAgents": "Agents",
|
||||||
"aiWorkflows": "AI Workflows",
|
"aiWorkflows": "AI Workflows",
|
||||||
"workflowRuns": "Workflow Audit",
|
"workflowRuns": "Workflow Audit",
|
||||||
|
"workflows": "Workflows",
|
||||||
"agentRuns": "Agent Audit",
|
"agentRuns": "Agent Audit",
|
||||||
"skillDefinition": "Skills",
|
"skillDefinition": "Skills",
|
||||||
"mcp": "MCP tools",
|
"mcp": "MCP tools",
|
||||||
|
|||||||
@@ -2340,6 +2340,7 @@
|
|||||||
"aiAgents": "Agent",
|
"aiAgents": "Agent",
|
||||||
"aiWorkflows": "AI流程",
|
"aiWorkflows": "AI流程",
|
||||||
"workflowRuns": "流程审计",
|
"workflowRuns": "流程审计",
|
||||||
|
"workflows": "工作流",
|
||||||
"agentRuns": "Agent 审计",
|
"agentRuns": "Agent 审计",
|
||||||
"skillDefinition": "Skills",
|
"skillDefinition": "Skills",
|
||||||
"mcp": "MCP tools",
|
"mcp": "MCP tools",
|
||||||
|
|||||||
Reference in New Issue
Block a user