"use client" import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react" import { ArrowDownIcon, ArrowUpIcon, BotMessageSquareIcon, GitBranchIcon, HistoryIcon, PlugIcon, 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, fetchAIAgentWorkflow, fetchAIConfigsAll, fetchAIWorkflowDefaultDefinition, fetchAIWorkflowNodeSpecs, fetchAIWorkflowVersions, fetchAgentTeamsAll, fetchKnowledgeBasesAll, fetchMCPCatalog, fetchSkillDefinitionsAll, publishAIAgentWorkflow, saveAIAgentWorkflow, updateAIAgent, validateAIWorkflow, type AIAgent, type AIConfig, type AIWorkflowDefinition, type AIWorkflowNodeSpec, 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 { useWorkflowDefinitionHistory } from "../../ai-workflows/_components/use-workflow-definition-history" 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 [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 [serviceMode, setServiceMode] = useState(String(IMConversationServiceMode.AIFirst)) const [systemPrompt, setSystemPrompt] = useState("") const [welcomeMessage, setWelcomeMessage] = useState("") const [replyTimeoutSeconds, setReplyTimeoutSeconds] = useState("180") const [handoffMode, setHandoffMode] = useState(String(AIAgentHandoffMode.WaitPool)) const [fallbackMode, setFallbackMode] = useState(String(AIAgentFallbackMode.NoAnswer)) const [fallbackMessage, setFallbackMessage] = useState("") const [selectedKnowledgeIds, setSelectedKnowledgeIds] = useState([]) const [selectedTeamIds, setSelectedTeamIds] = useState([]) const [selectedSkillIds, setSelectedSkillIds] = useState([]) const [directTools, setDirectTools] = useState([]) const { definition, revision: workflowRevision, canUndo: canUndoWorkflow, canRedo: canRedoWorkflow, replace: replaceWorkflowHistory, update: updateWorkflowDefinition, undo: undoWorkflowDefinition, redo: redoWorkflowDefinition, } = useWorkflowDefinitionHistory(fallbackDefinition) const [aiConfigs, setAIConfigs] = useState([]) const [knowledgeBases, setKnowledgeBases] = useState([]) const [agentTeams, setAgentTeams] = useState([]) const [skills, setSkills] = useState([]) const [toolCatalog, setToolCatalog] = useState([]) const [knowledgeToAdd, setKnowledgeToAdd] = useState("") const [teamToAdd, setTeamToAdd] = useState("") const [skillToAdd, setSkillToAdd] = useState("") const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("") const [directToolToAdd, setDirectToolToAdd] = useState("") useEffect(() => { setCurrentAgentId(agentId ?? null) }, [agentId]) const replaceWorkflowDefinition = useCallback((nextDefinition: AIWorkflowDefinition) => { replaceWorkflowHistory(nextDefinition) }, [replaceWorkflowHistory]) const loadData = useCallback(async () => { setLoading(true) try { const [ specs, defaultDefinition, configs, bases, teams, skillList, catalog, ] = await Promise.all([ fetchAIWorkflowNodeSpecs(), fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition), fetchAIConfigsAll({ modelType: AIModelType.LLM }), fetchKnowledgeBasesAll({ status: Status.Ok }), fetchAgentTeamsAll(), fetchSkillDefinitionsAll({ status: Status.Ok }), fetchMCPCatalog(), ]) setNodeSpecs(specs ?? []) setAIConfigs(configs ?? []) setKnowledgeBases(bases ?? []) setAgentTeams(teams ?? []) setSkills(skillList ?? []) setToolCatalog(catalog ?? []) if (!currentAgentId || currentAgentId <= 0) { setAgent(null) setWorkflowVersions([]) setName("") setDescription("") setAIConfigId("") setServiceMode(String(IMConversationServiceMode.AIFirst)) setSystemPrompt("") setWelcomeMessage("") setReplyTimeoutSeconds("180") setHandoffMode(String(AIAgentHandoffMode.WaitPool)) setFallbackMode(String(AIAgentFallbackMode.NoAnswer)) setFallbackMessage("") setSelectedKnowledgeIds([]) setSelectedTeamIds([]) setSelectedSkillIds([]) setDirectTools([]) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) return } const [agentDetail, workflowDetail] = await Promise.all([ fetchAIAgent(currentAgentId), fetchAIAgentWorkflow(currentAgentId), ]) setAgent(agentDetail) if (workflowDetail?.id > 0) { const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 }) setWorkflowVersions(versionPage.results ?? []) } else { setWorkflowVersions([]) } setName(agentDetail.name) setDescription(agentDetail.description || "") setAIConfigId(toText(agentDetail.aiConfigId)) setServiceMode(String(agentDetail.serviceMode || IMConversationServiceMode.AIFirst)) setSystemPrompt(agentDetail.systemPrompt || "") setWelcomeMessage(agentDetail.welcomeMessage || "") setReplyTimeoutSeconds(String(agentDetail.replyTimeoutSeconds ?? 180)) setHandoffMode(String(agentDetail.handoffMode || AIAgentHandoffMode.WaitPool)) setFallbackMode(String(agentDetail.fallbackMode || AIAgentFallbackMode.NoAnswer)) setFallbackMessage(agentDetail.fallbackMessage || "") setSelectedKnowledgeIds(agentDetail.knowledgeIds ?? []) setSelectedTeamIds((agentDetail.teams ?? []).map((team) => team.id)) setSelectedSkillIds(agentDetail.skillIds ?? []) setDirectTools(agentDetail.directTools ?? []) replaceWorkflowDefinition(workflowDetail.draftDefinition ?? 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 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: "引导用户补充信息" }, ], [] ) const aiConfigOptions = useMemo( () => aiConfigs.map((item) => ({ value: String(item.id), label: `${item.name} · ${item.modelName}` })), [aiConfigs] ) const knowledgeOptions = useMemo( () => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name })), [knowledgeBases] ) 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 directToolOptions = useMemo( () => toolCatalog .filter((tool) => !tool.autoInjected && tool.sourceType === "mcp") .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 moveKnowledge(index: number, direction: -1 | 1) { const targetIndex = index + direction if (targetIndex < 0 || targetIndex >= selectedKnowledgeIds.length) return const next = [...selectedKnowledgeIds] const current = next[index] next[index] = next[targetIndex] next[targetIndex] = current setSelectedKnowledgeIds(next) } 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 buildPayload(): CreateAIAgentPayload { return { name: name.trim(), description: description.trim(), aiConfigId: Number(aiConfigId), serviceMode: Number(serviceMode), systemPrompt: systemPrompt.trim(), welcomeMessage: welcomeMessage.trim(), replyTimeoutSeconds: Number(replyTimeoutSeconds), teamIds: uniqueNumbers(selectedTeamIds), handoffMode: Number(handoffMode), fallbackMode: Number(fallbackMode), fallbackMessage: fallbackMessage.trim(), knowledgeIds: uniqueNumbers(selectedKnowledgeIds), skillIds: uniqueNumbers(selectedSkillIds), directTools, } } 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 saveWorkflowDraft() { if (!currentAgentId) return setSavingWorkflow(true) try { await saveAIAgentWorkflow({ agentId: currentAgentId, 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 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) } } async function publishWorkflow() { if (!currentAgentId) return setSavingWorkflow(true) try { const saved = await saveAIAgentWorkflow({ agentId: currentAgentId, 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 selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions) const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions) const selectedSkillOptions = selectedOptions(selectedSkillIds, skillOptions) const workflowPublished = isWorkflowPublished(agent) const workflowStateText = agent?.workflowStateText || (workflowPublished ? "已发布" : "未发布") return (

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

{agent?.statusName ? {agent.statusName} : null} {workflowStateText} {workflowPublished ? ( 当前生效 #{agent?.workflowVersionId} ) : null}
{activeSection === "workflow" ? ( null ) : ( <> )}
{agent && !workflowPublished ? (
未发布流程,AI 不会自动回复。保存配置后请进入“会话流程”发布一个版本,再绑定渠道或启用自动回复。
) : null}
{sections.map((section) => ( ))}
{loading ? (
加载中...
) : (
{activeSection === "basic" ? (
setName(event.target.value)} />
) : null} {activeSection === "basic" ? (
setReplyTimeoutSeconds(event.target.value)} />
setSystemPrompt(next.raw)} />