From a628d103f996aa80421bc454bae4be49ee0380fc Mon Sep 17 00:00:00 2001 From: mlogclub Date: Wed, 24 Jun 2026 17:56:23 +0800 Subject: [PATCH] feat(workflow): enhance AI agent workflow state management and response handling --- .../application/runtime/tool_catalog_test.go | 2 +- .../application/runtime/workflow_runtime.go | 2 +- .../handlers/dashboard/ai_agent_handler.go | 17 +++++ .../dashboard/ai_agent_handler_test.go | 63 +++++++++++++++++++ internal/pkg/dto/response/ai_response.go | 3 + .../_components/config-workbench.tsx | 19 +++++- web/app/dashboard/ai-agents/page.tsx | 34 ++++++++++ .../dashboard/channels/_components/edit.tsx | 34 +++++++++- web/lib/api/admin.ts | 3 + 9 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 internal/handlers/dashboard/ai_agent_handler_test.go diff --git a/internal/ai/application/runtime/tool_catalog_test.go b/internal/ai/application/runtime/tool_catalog_test.go index 70bab59..812fcea 100644 --- a/internal/ai/application/runtime/tool_catalog_test.go +++ b/internal/ai/application/runtime/tool_catalog_test.go @@ -125,7 +125,7 @@ func TestPrepareWorkflowAgentRejectsMissingPublishedWorkflow(t *testing.T) { if err == nil { t.Fatalf("expected missing workflow version error") } - if !strings.Contains(err.Error(), "workflow version is required") { + if !strings.Contains(err.Error(), "AI Agent workflow is not published") { t.Fatalf("unexpected error: %v", err) } } diff --git a/internal/ai/application/runtime/workflow_runtime.go b/internal/ai/application/runtime/workflow_runtime.go index fe5466c..470923b 100644 --- a/internal/ai/application/runtime/workflow_runtime.go +++ b/internal/ai/application/runtime/workflow_runtime.go @@ -23,7 +23,7 @@ type resolvedWorkflow struct { func resolveAgentWorkflow(aiAgent models.AIAgent) (resolvedWorkflow, error) { if aiAgent.WorkflowVersionID <= 0 { - return resolvedWorkflow{}, errorsx.InvalidParam("workflow version is required") + return resolvedWorkflow{}, errorsx.InvalidParam("AI Agent workflow is not published; publish a workflow version before enabling automatic replies") } version := repositories.AIWorkflowVersionRepository.Get(sqls.DB(), aiAgent.WorkflowVersionID) if version == nil || version.Status != enums.StatusOk { diff --git a/internal/handlers/dashboard/ai_agent_handler.go b/internal/handlers/dashboard/ai_agent_handler.go index ede701a..0f5d734 100644 --- a/internal/handlers/dashboard/ai_agent_handler.go +++ b/internal/handlers/dashboard/ai_agent_handler.go @@ -190,6 +190,9 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons DirectTools: make([]response.AIAgentMCPToolResponse, 0), GraphTools: make([]string, 0), WorkflowVersionID: item.WorkflowVersionID, + WorkflowPublished: item.WorkflowVersionID > 0, + WorkflowState: aiAgentWorkflowState(item.WorkflowVersionID), + WorkflowStateText: aiAgentWorkflowStateText(item.WorkflowVersionID), SortNo: item.SortNo, CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"), @@ -283,6 +286,20 @@ func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) respons return ret } +func aiAgentWorkflowState(workflowVersionID int64) string { + if workflowVersionID > 0 { + return "published" + } + return "draft" +} + +func aiAgentWorkflowStateText(workflowVersionID int64) string { + if workflowVersionID > 0 { + return "已发布" + } + return "未发布" +} + func appendGraphToolCodeIfMissing(items []string, toolCode string) []string { toolCode = strings.TrimSpace(toolCode) if toolCode == "" { diff --git a/internal/handlers/dashboard/ai_agent_handler_test.go b/internal/handlers/dashboard/ai_agent_handler_test.go new file mode 100644 index 0000000..27ebbf7 --- /dev/null +++ b/internal/handlers/dashboard/ai_agent_handler_test.go @@ -0,0 +1,63 @@ +package dashboard + +import ( + "testing" + + "agent-desk/internal/models" + + "github.com/glebarez/sqlite" + "github.com/mlogclub/simple/sqls" + "gorm.io/gorm" +) + +func TestBuildAIAgentResponseExposesWorkflowPublishState(t *testing.T) { + setupAIAgentHandlerTestDB(t) + + draft := buildAIAgentResponse(&models.AIAgent{}) + if draft.WorkflowPublished { + t.Fatalf("draft.WorkflowPublished = true, want false") + } + if draft.WorkflowState != "draft" { + t.Fatalf("draft.WorkflowState = %q, want draft", draft.WorkflowState) + } + if draft.WorkflowStateText == "" { + t.Fatalf("expected draft workflow state text") + } + + published := buildAIAgentResponse(&models.AIAgent{WorkflowVersionID: 12}) + if !published.WorkflowPublished { + t.Fatalf("published.WorkflowPublished = false, want true") + } + if published.WorkflowState != "published" { + t.Fatalf("published.WorkflowState = %q, want published", published.WorkflowState) + } + if published.WorkflowStateText == "" { + t.Fatalf("expected published workflow state text") + } +} + +func setupAIAgentHandlerTestDB(t *testing.T) { + t.Helper() + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite db: %v", err) + } + sqlDB, err := db.DB() + if err != nil { + t.Fatalf("get sqlite db: %v", err) + } + t.Cleanup(func() { + if err := sqlDB.Close(); err != nil { + t.Fatalf("close sqlite db: %v", err) + } + }) + if err := db.AutoMigrate( + &models.AIConfig{}, + &models.AgentTeam{}, + &models.KnowledgeBase{}, + &models.SkillDefinition{}, + ); err != nil { + t.Fatalf("auto migrate: %v", err) + } + sqls.SetDB(db) +} diff --git a/internal/pkg/dto/response/ai_response.go b/internal/pkg/dto/response/ai_response.go index cc7ca62..7aad5d6 100644 --- a/internal/pkg/dto/response/ai_response.go +++ b/internal/pkg/dto/response/ai_response.go @@ -92,6 +92,9 @@ type AIAgentResponse struct { DirectTools []AIAgentMCPToolResponse `json:"directTools"` GraphTools []string `json:"graphTools"` WorkflowVersionID int64 `json:"workflowVersionId"` + WorkflowPublished bool `json:"workflowPublished"` + WorkflowState string `json:"workflowState"` + WorkflowStateText string `json:"workflowStateText"` SortNo int `json:"sortNo"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` diff --git a/web/app/dashboard/ai-agents/_components/config-workbench.tsx b/web/app/dashboard/ai-agents/_components/config-workbench.tsx index 3d7806b..17153b8 100644 --- a/web/app/dashboard/ai-agents/_components/config-workbench.tsx +++ b/web/app/dashboard/ai-agents/_components/config-workbench.tsx @@ -160,6 +160,10 @@ function uniqueNumbers(input: number[]) { return Array.from(new Set(input.filter((id) => Number.isFinite(id) && id > 0))) } +function isWorkflowPublished(agent: AIAgent | null) { + return Boolean(agent?.workflowPublished ?? (agent?.workflowVersionId ?? 0) > 0) +} + export function AIAgentConfigWorkbench({ agentId, onAgentSaved, @@ -544,6 +548,9 @@ export function AIAgentConfigWorkbench({ const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions) const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions) const selectedSkillOptions = selectedOptions(selectedSkillIds, skillOptions) + const workflowPublished = isWorkflowPublished(agent) + const workflowStateText = + agent?.workflowStateText || (workflowPublished ? "已发布" : "未发布") return (
@@ -555,7 +562,12 @@ export function AIAgentConfigWorkbench({

{agent?.name ?? "新建 AI Agent"}

{agent?.statusName ? {agent.statusName} : null} - {agent?.workflowVersionId ? 已发布 : 草稿} + + {workflowStateText} + + {workflowPublished ? ( + 当前生效 #{agent?.workflowVersionId} + ) : null}
@@ -618,6 +630,11 @@ export function AIAgentConfigWorkbench({
+ {agent && !workflowPublished ? ( +
+ 未发布流程,AI 不会自动回复。保存配置后请进入“会话流程”发布一个版本,再绑定渠道或启用自动回复。 +
+ ) : null}
{sections.map((section) => ( diff --git a/web/app/dashboard/ai-agents/page.tsx b/web/app/dashboard/ai-agents/page.tsx index e270c5d..ea5c08f 100644 --- a/web/app/dashboard/ai-agents/page.tsx +++ b/web/app/dashboard/ai-agents/page.tsx @@ -62,6 +62,10 @@ function getNextStatus(item: AIAgent) { return item.status === Status.Ok ? Status.Disabled : Status.Ok; } +function isWorkflowPublished(item: AIAgent) { + return Boolean(item.workflowPublished ?? item.workflowVersionId > 0); +} + export default function DashboardAIAgentsPage() { const t = useI18n(); const statusOptions = useMemo(() => getStatusOptions(t), [t]); @@ -116,6 +120,36 @@ export default function DashboardAIAgentsPage() { label: t("aiAgent.columnServiceMode"), render: (item) => getServiceModeLabel(item.serviceMode, t), }, + { + key: "workflow", + label: "流程状态", + render: (item) => { + const published = isWorkflowPublished(item); + return ( +
+
+ + {item.workflowStateText || (published ? "已发布" : "未发布")} + + {published ? ( + + #{item.workflowVersionId} + + ) : null} +
+ {!published ? ( +
+ 未发布流程,AI 不会自动回复 +
+ ) : ( +
+ 当前生效版本 #{item.workflowVersionId} +
+ )} +
+ ); + }, + }, { key: "knowledge", label: t("aiAgent.columnKnowledge"), diff --git a/web/app/dashboard/channels/_components/edit.tsx b/web/app/dashboard/channels/_components/edit.tsx index 399db77..3d1d379 100644 --- a/web/app/dashboard/channels/_components/edit.tsx +++ b/web/app/dashboard/channels/_components/edit.tsx @@ -10,6 +10,7 @@ import { toast } from "sonner" import { getWidgetDemoPath } from "@/components/support-chat/demo-navigation" import { OptionCombobox } from "@/components/option-combobox" import { ProjectDialog } from "@/components/project-dialog" +import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Field, @@ -246,6 +247,10 @@ function buildPayload(form: EditForm, status: number, t: Translate): CreateAdmin } } +function isAgentWorkflowPublished(agent: AIAgent | undefined) { + return Boolean(agent?.workflowPublished ?? (agent?.workflowVersionId ?? 0) > 0) +} + type ChannelFormBodyProps = Omit export function EditDialog({ @@ -313,6 +318,7 @@ function ChannelFormBody({ formState: { errors }, } = form const channelType = useWatch({ control, name: "channelType" }) + const aiAgentId = useWatch({ control, name: "aiAgentId" }) const openKfId = useWatch({ control, name: "openKfId" }) const userTokenSecret = useWatch({ control, name: "userTokenSecret" }) @@ -384,9 +390,15 @@ function ChannelFormBody({ t, ]) - const aiAgentOptions = aiAgents.map((item) => ({ + const selectedAIAgent = aiAgents.find((item) => String(item.id) === aiAgentId) + const availableAIAgents = aiAgents.filter( + (item) => isAgentWorkflowPublished(item) || String(item.id) === aiAgentId + ) + const aiAgentOptions = availableAIAgents.map((item) => ({ value: String(item.id), - label: item.name, + label: isAgentWorkflowPublished(item) + ? `${item.name} · 当前生效 #${item.workflowVersionId}` + : `${item.name} · 未发布`, })) const wxWorkKFAccountOptions = wxWorkKFAccounts.map((item) => ({ value: item.openKfId, @@ -413,6 +425,11 @@ function ChannelFormBody({ } async function onFormSubmit(values: EditForm) { + const selected = aiAgents.find((item) => String(item.id) === values.aiAgentId) + if (!isAgentWorkflowPublished(selected)) { + toast.error("该 Agent 尚未发布流程,不能绑定渠道") + return + } await onSubmit(buildPayload(values, currentStatus, t)) } @@ -504,6 +521,19 @@ function ChannelFormBody({ /> )} /> + {selectedAIAgent && !isAgentWorkflowPublished(selectedAIAgent) ? ( +
+ 该 Agent 尚未发布流程,AI 不会自动回复。请先在 Agent 配置中发布流程版本。 +
+ ) : null} + {selectedAIAgent && isAgentWorkflowPublished(selectedAIAgent) ? ( +
+ + {selectedAIAgent.workflowStateText || "已发布"} + + 当前生效版本 #{selectedAIAgent.workflowVersionId} +
+ ) : null} diff --git a/web/lib/api/admin.ts b/web/lib/api/admin.ts index 3167f44..1d71ec6 100644 --- a/web/lib/api/admin.ts +++ b/web/lib/api/admin.ts @@ -244,6 +244,9 @@ export type AIAgent = { }[] graphTools: string[] workflowVersionId: number + workflowPublished: boolean + workflowState: string + workflowStateText: string sortNo: number createdAt: string updatedAt: string