"use client" import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react" import { BotMessageSquareIcon, GitBranchIcon, HistoryIcon, PlugIcon, RotateCcwIcon, SaveIcon, SettingsIcon, Trash2Icon, } from "lucide-react" import { toast } from "sonner" import { ContentEditor } from "@/components/content-editor" import { OptionCombobox } from "@/components/option-combobox" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Textarea } from "@/components/ui/textarea" import { createAIAgent, fetchAIAgent, fetchAIAgentRevisions, fetchAIAgentWorkflow, fetchAIConfigsAll, fetchKnowledgeBasesAll, fetchAIWorkflowDefaultDefinition, fetchAIWorkflowNodeSpecs, fetchAIWorkflowTemplates, fetchAIWorkflowVersions, fetchAIWorkflows, fetchAgentTeamsAll, fetchMCPCatalog, fetchSkillDefinitionsAll, publishAIAgentWorkflow, publishAIAgent, rollbackAIAgent, rollbackAIAgentRollout, saveAIAgentWorkflow, updateAIAgent, validateAIWorkflow, type AIAgent, type AIAgentWorkflowBindingInput, type AgentRevision, type AIConfig, type AIWorkflowDefinition, type AIWorkflow, type AIWorkflowNodeSpec, type AIWorkflowTemplate, type AIWorkflowVersion, type AdminAgentTeam, type CreateAIAgentPayload, type KnowledgeBase, type MCPToolCatalogItem, type MCPToolSourceType, type SkillDefinition, } from "@/lib/api/admin" import { AIAgentFallbackMode, AIAgentHandoffMode, AIModelType, IMConversationServiceMode, Status, } from "@/lib/generated/enums" import { WorkflowEditor } from "../../ai-workflows/_components/workflow-editor" type DirectToolItem = CreateAIAgentPayload["directTools"][number] type DirectToolOption = { value: string label: string meta: DirectToolItem sourceType: MCPToolSourceType groupLabel: string } type SectionKey = | "basic" | "capabilities" | "workflow" const fallbackDefinition: 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" }], } function toText(value: string | number | undefined | null) { if (value === undefined || value === null || value === 0) return "" return String(value) } 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, onAgentCreated, }: { agentId?: number | null onAgentSaved?: () => void onAgentCreated?: (agent: AIAgent) => void }) { const [currentAgentId, setCurrentAgentId] = useState(agentId ?? null) const [activeSection, setActiveSection] = useState("basic") const [agent, setAgent] = useState(null) const [workflowVersions, setWorkflowVersions] = useState([]) const [agentRevisions, setAgentRevisions] = useState([]) const [nodeSpecs, setNodeSpecs] = useState([]) const [loading, setLoading] = useState(true) const [savingAgent, setSavingAgent] = useState(false) const [savingWorkflow, setSavingWorkflow] = useState(false) const [versionDialogOpen, setVersionDialogOpen] = useState(false) const [name, setName] = useState("") const [description, setDescription] = useState("") const [aiConfigId, setAIConfigId] = useState("") const [runtimeMode, setRuntimeMode] = useState<"workflow" | "autonomous" | "hybrid">("autonomous") const [serviceMode, setServiceMode] = useState(String(IMConversationServiceMode.AIFirst)) const [systemPrompt, setSystemPrompt] = useState("") const [welcomeMessage, setWelcomeMessage] = useState("") const [replyTimeoutSeconds, setReplyTimeoutSeconds] = useState("180") const [rolloutPercent, setRolloutPercent] = useState("5") const [handoffMode, setHandoffMode] = useState(String(AIAgentHandoffMode.WaitPool)) const [fallbackMode, setFallbackMode] = useState(String(AIAgentFallbackMode.NoAnswer)) const [fallbackMessage, setFallbackMessage] = useState("") const [selectedTeamIds, setSelectedTeamIds] = useState([]) const [selectedSkillIds, setSelectedSkillIds] = useState([]) const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]) const [directTools, setDirectTools] = useState([]) const [workflowBindings, setWorkflowBindings] = useState([]) const [publishedWorkflows, setPublishedWorkflows] = useState([]) const [workflowToAdd, setWorkflowToAdd] = useState("") const [definition, setDefinition] = useState(fallbackDefinition) const [workflowRevision, setWorkflowRevision] = useState(0) const [workflowTemplates, setWorkflowTemplates] = useState([]) const [selectedWorkflowTemplate, setSelectedWorkflowTemplate] = useState("") const [aiConfigs, setAIConfigs] = useState([]) const [agentTeams, setAgentTeams] = useState([]) const [skills, setSkills] = useState([]) const [knowledgeBases, setKnowledgeBases] = useState([]) const [toolCatalog, setToolCatalog] = useState([]) const [teamToAdd, setTeamToAdd] = useState("") const [skillToAdd, setSkillToAdd] = useState("") const [knowledgeBaseToAdd, setKnowledgeBaseToAdd] = useState("") const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("") const [directToolToAdd, setDirectToolToAdd] = useState("") const previousRolloutPercent = agent?.previousRolloutPercent ?? 0 useEffect(() => { setCurrentAgentId(agentId ?? null) }, [agentId]) const replaceWorkflowDefinition = useCallback((nextDefinition: AIWorkflowDefinition) => { setDefinition(nextDefinition) setWorkflowRevision((current) => current + 1) }, []) const loadData = useCallback(async () => { setLoading(true) try { const [ specs, defaultDefinition, templates, configs, teams, skillList, knowledgeBaseList, catalog, workflowPage, ] = await Promise.all([ fetchAIWorkflowNodeSpecs(), fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition), fetchAIWorkflowTemplates(), fetchAIConfigsAll({ modelType: AIModelType.LLM }), fetchAgentTeamsAll(), fetchSkillDefinitionsAll({ status: Status.Ok }), fetchKnowledgeBasesAll({ status: Status.Ok }), fetchMCPCatalog(), fetchAIWorkflows({ limit: 100 }), ]) setNodeSpecs(specs ?? []) setWorkflowTemplates(templates ?? []) setAIConfigs(configs ?? []) setAgentTeams(teams ?? []) setSkills(skillList ?? []) setKnowledgeBases(knowledgeBaseList ?? []) setToolCatalog(catalog ?? []) setPublishedWorkflows((workflowPage.results ?? []).filter((item) => item.publishedVersionId > 0)) if (!currentAgentId || currentAgentId <= 0) { setAgent(null) setWorkflowVersions([]) setAgentRevisions([]) setName("") setDescription("") setAIConfigId("") setRuntimeMode("autonomous") setServiceMode(String(IMConversationServiceMode.AIFirst)) setSystemPrompt("") setWelcomeMessage("") setReplyTimeoutSeconds("180") setRolloutPercent("5") setHandoffMode(String(AIAgentHandoffMode.WaitPool)) setFallbackMode(String(AIAgentFallbackMode.NoAnswer)) setFallbackMessage("") setSelectedTeamIds([]) setSelectedSkillIds([]) setSelectedKnowledgeBaseIds([]) setDirectTools([]) setWorkflowBindings([]) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) return } const [agentDetail, revisionList] = await Promise.all([ fetchAIAgent(currentAgentId), fetchAIAgentRevisions(currentAgentId), ]) setAgent(agentDetail) setAgentRevisions(revisionList ?? []) setWorkflowVersions([]) setName(agentDetail.name) setDescription(agentDetail.description || "") setAIConfigId(toText(agentDetail.aiConfigId)) setRuntimeMode(agentDetail.runtimeMode === "autonomous" || agentDetail.runtimeMode === "hybrid" ? agentDetail.runtimeMode : "workflow") setServiceMode(String(agentDetail.serviceMode || IMConversationServiceMode.AIFirst)) setSystemPrompt(agentDetail.systemPrompt || "") setWelcomeMessage(agentDetail.welcomeMessage || "") setReplyTimeoutSeconds(String(agentDetail.replyTimeoutSeconds ?? 180)) setRolloutPercent(String(agentDetail.rolloutPercent || 100)) setHandoffMode(String(agentDetail.handoffMode || AIAgentHandoffMode.WaitPool)) setFallbackMode(String(agentDetail.fallbackMode || AIAgentFallbackMode.NoAnswer)) setFallbackMessage(agentDetail.fallbackMessage || "") setSelectedTeamIds((agentDetail.teams ?? []).map((team) => team.id)) setSelectedSkillIds(agentDetail.skillIds ?? []) setSelectedKnowledgeBaseIds(agentDetail.knowledgeBaseIds ?? []) setDirectTools(agentDetail.directTools ?? []) setWorkflowBindings((agentDetail.workflowBindings ?? []).map(({ workflowVersionId, toolName, triggerInstruction, priority, enabled }) => ({ workflowVersionId, toolName, triggerInstruction, priority, enabled }))) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to load Agent config") } finally { setLoading(false) } }, [currentAgentId, replaceWorkflowDefinition]) useEffect(() => { void loadData() }, [loadData]) const serviceModeOptions = useMemo( () => [ { value: String(IMConversationServiceMode.AIOnly), label: "仅 AI" }, { value: String(IMConversationServiceMode.HumanOnly), label: "仅人工" }, { value: String(IMConversationServiceMode.AIFirst), label: "AI 优先" }, ], [] ) const runtimeModeOptions = useMemo( () => [ { value: "autonomous", label: "自主接待" }, { value: "hybrid", label: "自主接待 + 工作流" }, { value: "workflow", label: "仅工作流" }, ], [] ) const handoffModeOptions = useMemo( () => [ { value: String(AIAgentHandoffMode.WaitPool), label: "进入待接入池" }, { value: String(AIAgentHandoffMode.DefaultTeamPool), label: "进入默认客服组待接入池" }, { value: String(AIAgentHandoffMode.AIHoldAndNotify), label: "AI继续接待并提醒人工" }, ], [] ) const fallbackModeOptions = useMemo( () => [ { value: String(AIAgentFallbackMode.NoAnswer), label: "直接说明知识不足" }, { value: String(AIAgentFallbackMode.SuggestRetry), label: "引导用户补充信息" }, { value: String(AIAgentFallbackMode.Handoff), label: "转人工客服" }, ], [] ) const aiConfigOptions = useMemo( () => aiConfigs.map((item) => ({ value: String(item.id), label: `${item.name} · ${item.modelName}` })), [aiConfigs] ) const teamOptions = useMemo( () => agentTeams.map((item) => ({ value: String(item.id), label: item.name })), [agentTeams] ) const skillOptions = useMemo( () => skills.map((item) => ({ value: String(item.id), label: item.name })), [skills] ) const knowledgeBaseOptions = useMemo( () => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name })), [knowledgeBases] ) const directToolOptions = useMemo( () => toolCatalog .filter( (tool) => !tool.autoInjected && (tool.sourceType === "mcp" || tool.toolCode === "builtin/conversation_context" || tool.toolCode === "graph/prepare_ticket_draft") ) .map((tool) => ({ value: tool.toolCode, label: `${tool.title || tool.toolName} · ${tool.toolCode}`, sourceType: tool.sourceType, groupLabel: tool.sourceType === "builtin" ? "内置工具" : tool.serverCode, meta: { toolCode: tool.toolCode, serverCode: tool.serverCode, toolName: tool.toolName, title: tool.title || tool.toolName, description: tool.description || "", arguments: undefined, }, })), [toolCatalog] ) const directToolGroupOptions = useMemo( () => Array.from( new Map( directToolOptions.map((option) => [ option.groupLabel, { value: option.groupLabel, label: option.groupLabel }, ]) ).values() ), [directToolOptions] ) const addableDirectToolOptions = useMemo( () => directToolOptions.filter( (option) => option.groupLabel === directToolGroupToAdd && !directTools.some((tool) => tool.toolCode === option.value) ), [directToolGroupToAdd, directToolOptions, directTools] ) function selectedOptions(ids: number[], options: { value: string; label: string }[]) { return ids .map((id) => options.find((option) => Number(option.value) === id)) .filter((option): option is { value: string; label: string } => !!option) } function addSelected(value: string, current: number[], setNext: (ids: number[]) => void) { const id = Number(value) if (!Number.isFinite(id) || id <= 0 || current.includes(id)) return setNext([...current, id]) } function addDirectTool(value: string) { const option = directToolOptions.find((item) => item.value === value) if (!option) return setDirectTools((current) => current.some((tool) => tool.toolCode === option.meta.toolCode) ? current : [...current, option.meta] ) 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 { return { name: name.trim(), description: description.trim(), aiConfigId: Number(aiConfigId), runtimeMode, serviceMode: Number(serviceMode), systemPrompt: systemPrompt.trim(), welcomeMessage: welcomeMessage.trim(), replyTimeoutSeconds: Number(replyTimeoutSeconds), rolloutPercent: Number(rolloutPercent), teamIds: uniqueNumbers(selectedTeamIds), handoffMode: Number(handoffMode), fallbackMode: Number(fallbackMode), fallbackMessage: fallbackMessage.trim(), knowledgeBaseIds: uniqueNumbers(selectedKnowledgeBaseIds), skillIds: uniqueNumbers(selectedSkillIds), directTools, workflowBindings, } } async function saveAgentSettings() { setSavingAgent(true) try { const payload = buildPayload() if (agent) { await updateAIAgent({ id: agent.id, ...payload }) toast.success("Agent config saved") await loadData() } else { const created = await createAIAgent(payload) setCurrentAgentId(created.id) setAgent(created) toast.success("Agent created") onAgentCreated?.(created) } onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to save Agent config") } finally { setSavingAgent(false) } } async function publishAutonomousAgent() { if (!agent || (runtimeMode !== "autonomous" && runtimeMode !== "hybrid")) return setSavingAgent(true) try { await publishAIAgent(agent.id) await loadData() toast.success("Agent published") } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to publish Autonomous Agent") } finally { setSavingAgent(false) } } async function saveWorkflowDraft() { if (!currentAgentId) return setSavingWorkflow(true) try { await saveAIAgentWorkflow({ name: "", description: "", definition, }) toast.success("Workflow draft saved") } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to save workflow draft") } finally { setSavingWorkflow(false) } } async function rollbackAgentRevision(revisionId: number) { if (!agent || revisionId <= 0 || revisionId === agent.publishedRevisionId) return setSavingAgent(true) try { await rollbackAIAgent(agent.id, revisionId) toast.success("已回滚到选中的 Agent 版本") await loadData() onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "回滚 Agent 版本失败") } finally { setSavingAgent(false) } } async function rollbackAgentRollout() { if (!agent || agent.previousRolloutPercent < 1) return setSavingAgent(true) try { await rollbackAIAgentRollout(agent.id) toast.success("已恢复上一次灰度比例") await loadData() onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "恢复灰度比例失败") } finally { setSavingAgent(false) } } async function validateWorkflowDraft() { setSavingWorkflow(true) try { const result = await validateAIWorkflow(definition) toast[result.valid ? "success" : "error"]( result.valid ? "Workflow is valid" : "Workflow has validation errors" ) } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to validate workflow") } finally { setSavingWorkflow(false) } } async function restoreDefaultWorkflow() { if (savingWorkflow || loading) return setSavingWorkflow(true) try { const defaultDefinition = await fetchAIWorkflowDefaultDefinition() replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) toast.success("已恢复默认流程,保存草稿或发布后生效") } catch (error) { toast.error(error instanceof Error ? error.message : "恢复默认流程失败") } finally { setSavingWorkflow(false) } } function applySelectedWorkflowTemplate() { const template = workflowTemplates.find((item) => item.code === selectedWorkflowTemplate) if (!template) return replaceWorkflowDefinition(template.definition) toast.success(`已应用 ${template.name} 模板,保存草稿或发布后生效`) } async function publishWorkflow() { if (!currentAgentId) return setSavingWorkflow(true) try { const saved = await saveAIAgentWorkflow({ name: "", description: "", definition, }) const version = await publishAIAgentWorkflow(currentAgentId, definition) toast.success(`Published version ${version.version}`) setAgent((current) => current ? { ...current, workflowVersionId: version.id } : current ) if (saved.id > 0) { const versionPage = await fetchAIWorkflowVersions({ workflowId: saved.id, limit: 20, }) setWorkflowVersions(versionPage.results ?? []) } else { setWorkflowVersions((current) => current.some((item) => item.id === version.id) ? current : [version, ...current] ) } onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to publish workflow") } finally { setSavingWorkflow(false) } } const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [ { key: "basic", title: "基础信息", icon: }, { key: "capabilities", title: "能力来源", icon: }, { key: "workflow", title: "关联工作流", icon: }, ] const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions) const selectedSkillOptions = selectedOptions(selectedSkillIds, skillOptions) const workflowPublished = isWorkflowPublished(agent) const autonomousPublished = runtimeMode === "autonomous" && (agent?.publishedRevisionId ?? 0) > 0 const hybridPublished = runtimeMode === "hybrid" && workflowPublished && (agent?.publishedRevisionId ?? 0) > 0 const runtimePublished = runtimeMode === "workflow" ? workflowPublished : runtimeMode === "hybrid" ? hybridPublished : autonomousPublished const workflowStateText = agent?.workflowStateText || (workflowPublished ? "已发布" : "未发布") return (

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

{agent?.statusName ? {agent.statusName} : null} {runtimeMode === "autonomous" ? (autonomousPublished ? "已发布" : "未发布") : runtimeMode === "hybrid" ? (hybridPublished ? "已发布" : "未发布") : workflowStateText} {workflowPublished ? ( 当前生效 #{agent?.workflowVersionId} ) : null}
{activeSection === "workflow" ? ( null ) : ( <> {agent && (runtimeMode === "autonomous" || runtimeMode === "hybrid") ? : null} )}
{agent && !runtimePublished ? (
{runtimeMode === "autonomous" ? "未发布 Agent,AI 不会自动回复。保存配置后发布 Agent,再绑定渠道或启用自动回复。" : runtimeMode === "hybrid" ? "未发布 Hybrid Agent,AI 不会自动回复。保存配置后请关联已发布工作流,再发布 Agent。" : "未发布工作流,AI 不会自动回复。请先关联一个已发布工作流。"}
) : null}
{sections.map((section) => ( ))}
{loading ? (
加载中...
) : (
{activeSection === "basic" ? (
setName(event.target.value)} />
) : null} {activeSection === "basic" ? (
setRuntimeMode(value === "autonomous" || value === "hybrid" ? value : "workflow")} /> setReplyTimeoutSeconds(event.target.value)} />
setRolloutPercent(event.target.value)} /> {previousRolloutPercent > 0 ? ( ) : null}
setSystemPrompt(next.raw)} />