feat(workflow): enhance AI agent workflow state management and response handling

This commit is contained in:
mlogclub
2026-06-24 17:56:23 +08:00
parent 0b5a24975c
commit a628d103f9
9 changed files with 172 additions and 5 deletions
@@ -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)
}
}
@@ -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 {
@@ -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 == "" {
@@ -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)
}
+3
View File
@@ -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"`
@@ -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 (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-background">
@@ -555,7 +562,12 @@ export function AIAgentConfigWorkbench({
<div className="flex min-w-0 items-center gap-2">
<h1 className="truncate text-base font-semibold">{agent?.name ?? "新建 AI Agent"}</h1>
{agent?.statusName ? <Badge variant="secondary">{agent.statusName}</Badge> : null}
{agent?.workflowVersionId ? <Badge></Badge> : <Badge variant="outline">稿</Badge>}
<Badge variant={workflowPublished ? "default" : "outline"}>
{workflowStateText}
</Badge>
{workflowPublished ? (
<Badge variant="secondary"> #{agent?.workflowVersionId}</Badge>
) : null}
</div>
</div>
<div className="flex items-center gap-2">
@@ -618,6 +630,11 @@ export function AIAgentConfigWorkbench({
</div>
<div className="flex min-h-0 flex-1 flex-col bg-background">
{agent && !workflowPublished ? (
<div className="shrink-0 border-b border-amber-200 bg-amber-50 px-5 py-2 text-sm text-amber-900">
AI
</div>
) : null}
<div className="shrink-0 border-b bg-muted/20 px-4 py-2">
<div className="flex min-w-0 items-center gap-1 overflow-x-auto overflow-y-hidden">
{sections.map((section) => (
+34
View File
@@ -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 (
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant={published ? "default" : "outline"}>
{item.workflowStateText || (published ? "已发布" : "未发布")}
</Badge>
{published ? (
<span className="font-mono text-xs text-muted-foreground">
#{item.workflowVersionId}
</span>
) : null}
</div>
{!published ? (
<div className="text-xs text-muted-foreground">
AI
</div>
) : (
<div className="text-xs text-muted-foreground">
#{item.workflowVersionId}
</div>
)}
</div>
);
},
},
{
key: "knowledge",
label: t("aiAgent.columnKnowledge"),
@@ -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<ChannelFormDialogProps, "open">
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) ? (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
Agent AI Agent
</div>
) : null}
{selectedAIAgent && isAgentWorkflowPublished(selectedAIAgent) ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="secondary">
{selectedAIAgent.workflowStateText || "已发布"}
</Badge>
<span> #{selectedAIAgent.workflowVersionId}</span>
</div>
) : null}
<FieldError errors={[errors.aiAgentId]} />
</FieldContent>
</Field>
+3
View File
@@ -244,6 +244,9 @@ export type AIAgent = {
}[]
graphTools: string[]
workflowVersionId: number
workflowPublished: boolean
workflowState: string
workflowStateText: string
sortNo: number
createdAt: string
updatedAt: string