feat: enhance AIAgentConfigWorkbench with version history dialog and refactor sections

This commit is contained in:
mlogclub
2026-06-26 17:28:54 +08:00
parent 9fd503b52b
commit aa346ac717
2 changed files with 135 additions and 125 deletions
@@ -5,16 +5,11 @@ import {
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
BotMessageSquareIcon, BotMessageSquareIcon,
BrainCircuitIcon,
DatabaseIcon,
GitBranchIcon, GitBranchIcon,
HistoryIcon, HistoryIcon,
LifeBuoyIcon,
PlugIcon, PlugIcon,
SaveIcon, SaveIcon,
SendIcon,
SettingsIcon, SettingsIcon,
ShieldCheckIcon,
Trash2Icon, Trash2Icon,
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
@@ -23,6 +18,12 @@ import { ContentEditor } from "@/components/content-editor"
import { OptionCombobox } from "@/components/option-combobox" import { OptionCombobox } from "@/components/option-combobox"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { import {
@@ -52,11 +53,9 @@ import {
validateAIWorkflow, validateAIWorkflow,
type AIAgent, type AIAgent,
type AIConfig, type AIConfig,
type AIWorkflow,
type AIWorkflowDefinition, type AIWorkflowDefinition,
type AIWorkflowNodeSpec, type AIWorkflowNodeSpec,
type AIWorkflowVersion, type AIWorkflowVersion,
type AIWorkflowValidationResult,
type AdminAgentTeam, type AdminAgentTeam,
type CreateAIAgentPayload, type CreateAIAgentPayload,
type KnowledgeBase, type KnowledgeBase,
@@ -85,13 +84,8 @@ type DirectToolOption = {
type SectionKey = type SectionKey =
| "basic" | "basic"
| "model" | "capabilities"
| "knowledge"
| "skills"
| "tools"
| "workflow" | "workflow"
| "handoff"
| "versions"
const fallbackDefinition: AIWorkflowDefinition = { const fallbackDefinition: AIWorkflowDefinition = {
schemaVersion: 1, schemaVersion: 1,
@@ -140,13 +134,12 @@ export function AIAgentConfigWorkbench({
const [currentAgentId, setCurrentAgentId] = useState(agentId ?? null) const [currentAgentId, setCurrentAgentId] = useState(agentId ?? null)
const [activeSection, setActiveSection] = useState<SectionKey>("basic") const [activeSection, setActiveSection] = useState<SectionKey>("basic")
const [agent, setAgent] = useState<AIAgent | null>(null) const [agent, setAgent] = useState<AIAgent | null>(null)
const [workflow, setWorkflow] = useState<AIWorkflow | null>(null)
const [workflowVersions, setWorkflowVersions] = useState<AIWorkflowVersion[]>([]) const [workflowVersions, setWorkflowVersions] = useState<AIWorkflowVersion[]>([])
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([]) const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
const [validation, setValidation] = useState<AIWorkflowValidationResult | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [savingAgent, setSavingAgent] = useState(false) const [savingAgent, setSavingAgent] = useState(false)
const [savingWorkflow, setSavingWorkflow] = useState(false) const [savingWorkflow, setSavingWorkflow] = useState(false)
const [versionDialogOpen, setVersionDialogOpen] = useState(false)
const [name, setName] = useState("") const [name, setName] = useState("")
const [description, setDescription] = useState("") const [description, setDescription] = useState("")
@@ -216,7 +209,6 @@ export function AIAgentConfigWorkbench({
if (!currentAgentId || currentAgentId <= 0) { if (!currentAgentId || currentAgentId <= 0) {
setAgent(null) setAgent(null)
setWorkflow(null)
setWorkflowVersions([]) setWorkflowVersions([])
setName("") setName("")
setDescription("") setDescription("")
@@ -233,7 +225,6 @@ export function AIAgentConfigWorkbench({
setSelectedSkillIds([]) setSelectedSkillIds([])
setDirectTools([]) setDirectTools([])
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
setValidation(null)
return return
} }
@@ -243,7 +234,6 @@ export function AIAgentConfigWorkbench({
]) ])
setAgent(agentDetail) setAgent(agentDetail)
setWorkflow(workflowDetail)
if (workflowDetail?.id > 0) { if (workflowDetail?.id > 0) {
const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 }) const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 })
setWorkflowVersions(versionPage.results ?? []) setWorkflowVersions(versionPage.results ?? [])
@@ -265,7 +255,6 @@ export function AIAgentConfigWorkbench({
setSelectedSkillIds(agentDetail.skillIds ?? []) setSelectedSkillIds(agentDetail.skillIds ?? [])
setDirectTools(agentDetail.directTools ?? []) setDirectTools(agentDetail.directTools ?? [])
replaceWorkflowDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition) replaceWorkflowDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition)
setValidation(null)
} 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 {
@@ -438,13 +427,12 @@ export function AIAgentConfigWorkbench({
if (!currentAgentId) return if (!currentAgentId) return
setSavingWorkflow(true) setSavingWorkflow(true)
try { try {
const saved = await saveAIAgentWorkflow({ await saveAIAgentWorkflow({
agentId: currentAgentId, agentId: currentAgentId,
name: "", name: "",
description: "", description: "",
definition, definition,
}) })
setWorkflow(saved)
toast.success("Workflow draft saved") toast.success("Workflow draft saved")
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to save workflow draft") toast.error(error instanceof Error ? error.message : "Failed to save workflow draft")
@@ -457,7 +445,6 @@ export function AIAgentConfigWorkbench({
setSavingWorkflow(true) setSavingWorkflow(true)
try { try {
const result = await validateAIWorkflow(definition) const result = await validateAIWorkflow(definition)
setValidation(result)
toast[result.valid ? "success" : "error"]( toast[result.valid ? "success" : "error"](
result.valid ? "Workflow is valid" : "Workflow has validation errors" result.valid ? "Workflow is valid" : "Workflow has validation errors"
) )
@@ -474,7 +461,6 @@ export function AIAgentConfigWorkbench({
try { try {
const defaultDefinition = await fetchAIWorkflowDefaultDefinition() const defaultDefinition = await fetchAIWorkflowDefaultDefinition()
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
setValidation(null)
toast.success("已恢复默认流程,保存草稿或发布后生效") toast.success("已恢复默认流程,保存草稿或发布后生效")
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "恢复默认流程失败") toast.error(error instanceof Error ? error.message : "恢复默认流程失败")
@@ -493,15 +479,11 @@ export function AIAgentConfigWorkbench({
description: "", description: "",
definition, definition,
}) })
setWorkflow(saved)
const version = await publishAIAgentWorkflow(currentAgentId, definition) const version = await publishAIAgentWorkflow(currentAgentId, definition)
toast.success(`Published version ${version.version}`) toast.success(`Published version ${version.version}`)
setAgent((current) => setAgent((current) =>
current ? { ...current, workflowVersionId: version.id } : current current ? { ...current, workflowVersionId: version.id } : current
) )
setWorkflow((current) =>
current ? { ...current, publishedVersionId: version.id } : saved
)
if (saved.id > 0) { if (saved.id > 0) {
const versionPage = await fetchAIWorkflowVersions({ const versionPage = await fetchAIWorkflowVersions({
workflowId: saved.id, workflowId: saved.id,
@@ -523,13 +505,8 @@ 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: "model", title: "模型与 Prompt", icon: <BrainCircuitIcon /> }, { key: "capabilities", title: "能力来源", icon: <PlugIcon /> },
{ key: "knowledge", title: "知识库", icon: <DatabaseIcon /> },
{ key: "skills", title: "Skills", icon: <ShieldCheckIcon /> },
{ key: "tools", title: "MCP Tools", icon: <PlugIcon /> },
{ key: "workflow", title: "会话流程", icon: <GitBranchIcon /> }, { key: "workflow", title: "会话流程", icon: <GitBranchIcon /> },
{ key: "handoff", title: "转人工与兜底", icon: <LifeBuoyIcon /> },
{ key: "versions", title: "版本记录", icon: <HistoryIcon /> },
] ]
const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions) const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions)
@@ -569,15 +546,7 @@ export function AIAgentConfigWorkbench({
onClick={saveAgentSettings} onClick={saveAgentSettings}
> >
<SaveIcon className="size-4" /> <SaveIcon className="size-4" />
保存 保存配置
</Button>
<Button
type="button"
disabled={savingWorkflow || loading || !currentAgentId}
onClick={publishWorkflow}
>
<SendIcon className="size-4" />
发布流程
</Button> </Button>
</> </>
)} )}
@@ -624,7 +593,7 @@ export function AIAgentConfigWorkbench({
加载中... 加载中...
</div> </div>
) : ( ) : (
<div className={activeSection === "workflow" ? "h-full min-h-0" : "w-full p-6"}> <div className={activeSection === "workflow" ? "h-full min-h-0" : "w-full space-y-6 p-6"}>
{activeSection === "basic" ? ( {activeSection === "basic" ? (
<ConfigSection> <ConfigSection>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
@@ -646,7 +615,7 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "model" ? ( {activeSection === "basic" ? (
<ConfigSection> <ConfigSection>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<FieldBlock label="AI 配置"> <FieldBlock label="AI 配置">
@@ -683,7 +652,38 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "knowledge" ? ( {activeSection === "basic" ? (
<ConfigSection>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<FieldBlock label="转人工模式">
<OptionCombobox value={handoffMode} options={handoffModeOptions} placeholder="选择转人工模式" onChange={setHandoffMode} />
</FieldBlock>
<FieldBlock label="兜底策略">
<OptionCombobox value={fallbackMode} options={fallbackModeOptions} placeholder="选择兜底策略" onChange={setFallbackMode} />
</FieldBlock>
</div>
<AddRow
value={teamToAdd}
options={teamOptions.filter((option) => !selectedTeamIds.includes(Number(option.value)))}
placeholder="选择客服组"
onValueChange={setTeamToAdd}
onAdd={() => {
addSelected(teamToAdd, selectedTeamIds, setSelectedTeamIds)
setTeamToAdd("")
}}
/>
<BadgeList
empty="未配置客服组。"
items={selectedTeamOptions}
onRemove={(id) => setSelectedTeamIds((current) => current.filter((item) => item !== id))}
/>
<FieldBlock label="兜底文案">
<Textarea rows={5} value={fallbackMessage} onChange={(event) => setFallbackMessage(event.target.value)} />
</FieldBlock>
</ConfigSection>
) : null}
{activeSection === "capabilities" ? (
<ConfigSection> <ConfigSection>
<AddRow <AddRow
value={knowledgeToAdd} value={knowledgeToAdd}
@@ -736,7 +736,7 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "skills" ? ( {activeSection === "capabilities" ? (
<ConfigSection> <ConfigSection>
<AddRow <AddRow
value={skillToAdd} value={skillToAdd}
@@ -756,7 +756,7 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "tools" ? ( {activeSection === "capabilities" ? (
<ConfigSection> <ConfigSection>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-[220px_minmax(0,1fr)_auto]"> <div className="grid grid-cols-1 gap-3 lg:grid-cols-[220px_minmax(0,1fr)_auto]">
<OptionCombobox <OptionCombobox
@@ -824,87 +824,32 @@ export function AIAgentConfigWorkbench({
saveDraftDisabled={savingWorkflow || loading || !currentAgentId} saveDraftDisabled={savingWorkflow || loading || !currentAgentId}
onPublish={publishWorkflow} onPublish={publishWorkflow}
publishDisabled={savingWorkflow || loading || !currentAgentId} 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>
}
/> />
) : null} ) : null}
{activeSection === "handoff" ? ( <Dialog open={versionDialogOpen} onOpenChange={setVersionDialogOpen}>
<ConfigSection> <DialogContent className="max-h-[80vh] overflow-hidden sm:max-w-4xl">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <DialogHeader>
<FieldBlock label="转人工模式"> <DialogTitle>版本记录</DialogTitle>
<OptionCombobox value={handoffMode} options={handoffModeOptions} placeholder="选择转人工模式" onChange={setHandoffMode} /> </DialogHeader>
</FieldBlock> <VersionRecordsTable
<FieldBlock label="兜底策略"> agent={agent}
<OptionCombobox value={fallbackMode} options={fallbackModeOptions} placeholder="选择兜底策略" onChange={setFallbackMode} /> workflowVersions={workflowVersions}
</FieldBlock>
</div>
<AddRow
value={teamToAdd}
options={teamOptions.filter((option) => !selectedTeamIds.includes(Number(option.value)))}
placeholder="选择客服组"
onValueChange={setTeamToAdd}
onAdd={() => {
addSelected(teamToAdd, selectedTeamIds, setSelectedTeamIds)
setTeamToAdd("")
}}
/> />
<BadgeList </DialogContent>
empty="未配置客服组。" </Dialog>
items={selectedTeamOptions}
onRemove={(id) => setSelectedTeamIds((current) => current.filter((item) => item !== id))}
/>
<FieldBlock label="兜底文案">
<Textarea rows={5} value={fallbackMessage} onChange={(event) => setFallbackMessage(event.target.value)} />
</FieldBlock>
</ConfigSection>
) : null}
{activeSection === "versions" ? (
<ConfigSection>
<div className="overflow-hidden rounded-md border">
{workflowVersions.length > 0 ? (
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
<TableHead className="w-28">版本</TableHead>
<TableHead>发布时间</TableHead>
<TableHead>发布人</TableHead>
<TableHead>状态</TableHead>
<TableHead className="text-right">定义指纹</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workflowVersions.map((version) => (
<TableRow key={version.id}>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">v{version.version}</span>
{agent?.workflowVersionId === version.id ? (
<Badge variant="secondary">当前生效</Badge>
) : null}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{version.publishedAt || version.createdAt || "-"}
</TableCell>
<TableCell>{version.publishedByName || "-"}</TableCell>
<TableCell>
<Badge variant={version.status === Status.Ok ? "outline" : "secondary"}>
{version.status === Status.Ok ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell className="text-right font-mono text-xs text-muted-foreground">
{version.definitionHash ? version.definitionHash.slice(0, 8) : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="p-4 text-sm text-muted-foreground">暂无已发布版本。</div>
)}
</div>
</ConfigSection>
) : null}
</div> </div>
)} )}
@@ -960,6 +905,60 @@ function AddRow({
) )
} }
function VersionRecordsTable({
agent,
workflowVersions,
}: {
agent: AIAgent | null
workflowVersions: AIWorkflowVersion[]
}) {
return (
<div className="max-h-[60vh] overflow-auto rounded-md border">
{workflowVersions.length > 0 ? (
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
<TableHead className="w-28">版本</TableHead>
<TableHead>发布时间</TableHead>
<TableHead>发布人</TableHead>
<TableHead>状态</TableHead>
<TableHead className="text-right">定义指纹</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workflowVersions.map((version) => (
<TableRow key={version.id}>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">v{version.version}</span>
{agent?.workflowVersionId === version.id ? (
<Badge variant="secondary">当前生效</Badge>
) : null}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{version.publishedAt || version.createdAt || "-"}
</TableCell>
<TableCell>{version.publishedByName || "-"}</TableCell>
<TableCell>
<Badge variant={version.status === Status.Ok ? "outline" : "secondary"}>
{version.status === Status.Ok ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell className="text-right font-mono text-xs text-muted-foreground">
{version.definitionHash ? version.definitionHash.slice(0, 8) : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="p-4 text-sm text-muted-foreground">暂无已发布版本。</div>
)}
</div>
)
}
function BadgeList({ function BadgeList({
empty, empty,
items, items,
@@ -40,7 +40,7 @@ import {
SendIcon, SendIcon,
Undo2Icon, Undo2Icon,
} from "lucide-react" } from "lucide-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
@@ -192,6 +192,7 @@ export function WorkflowEditor({
saveDraftDisabled = false, saveDraftDisabled = false,
onPublish, onPublish,
publishDisabled = false, publishDisabled = false,
toolbarExtra,
}: { }: {
definition: AIWorkflowDefinition definition: AIWorkflowDefinition
nodeSpecs: AIWorkflowNodeSpec[] nodeSpecs: AIWorkflowNodeSpec[]
@@ -204,6 +205,7 @@ export function WorkflowEditor({
saveDraftDisabled?: boolean saveDraftDisabled?: boolean
onPublish?: () => void onPublish?: () => void
publishDisabled?: boolean publishDisabled?: boolean
toolbarExtra?: ReactNode
}) { }) {
const [nodes, setNodes, onNodesChange] = useNodesState<WorkflowFlowNode>( const [nodes, setNodes, onNodesChange] = useNodesState<WorkflowFlowNode>(
toFlowNodes(definition) toFlowNodes(definition)
@@ -916,6 +918,7 @@ export function WorkflowEditor({
onRedo={redoWorkflowEdit} onRedo={redoWorkflowEdit}
onRestoreDefault={onRestoreDefault} onRestoreDefault={onRestoreDefault}
restoreDefaultDisabled={restoreDefaultDisabled} restoreDefaultDisabled={restoreDefaultDisabled}
toolbarExtra={toolbarExtra}
/> />
</div> </div>
{propertyPanelNode ? ( {propertyPanelNode ? (
@@ -1403,6 +1406,7 @@ function WorkflowCanvasToolbar({
onRedo, onRedo,
onRestoreDefault, onRestoreDefault,
restoreDefaultDisabled, restoreDefaultDisabled,
toolbarExtra,
}: { }: {
validationErrors: string[] validationErrors: string[]
validationValid: boolean validationValid: boolean
@@ -1418,6 +1422,7 @@ function WorkflowCanvasToolbar({
onRedo: () => void onRedo: () => void
onRestoreDefault?: () => void onRestoreDefault?: () => void
restoreDefaultDisabled?: boolean restoreDefaultDisabled?: boolean
toolbarExtra?: ReactNode
}) { }) {
return ( return (
<div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm"> <div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm">
@@ -1486,6 +1491,12 @@ function WorkflowCanvasToolbar({
</Button> </Button>
</> </>
) : null} ) : null}
{toolbarExtra ? (
<>
<WorkflowToolbarDivider />
{toolbarExtra}
</>
) : null}
<WorkflowToolbarDivider /> <WorkflowToolbarDivider />
<Button <Button
type="button" type="button"