"use client" import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react" import { HistoryIcon, MessageSquareTextIcon, PlugIcon, SaveIcon, SettingsIcon, Trash2Icon, UserRoundCheckIcon, } from "lucide-react" import { toast } from "sonner" import { ContentEditor } from "@/components/content-editor" 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 { 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, fetchAIConfigsAll, fetchAIWorkflows, fetchAgentTeamsAll, fetchKnowledgeBasesAll, fetchMCPCatalog, fetchSkillDefinitionsAll, publishAIAgent, rollbackAIAgent, updateAIAgent, type AIAgent, type AIAgentWorkflowBindingInput, type AgentRevision, type AIConfig, type AIWorkflow, type AdminAgentTeam, type CreateAIAgentPayload, type KnowledgeBase, type MCPToolCatalogItem, type SkillDefinition, } from "@/lib/api/admin" import { AIAgentFallbackMode, AIAgentHandoffMode, AIModelType, IMConversationServiceMode, Status, } from "@/lib/generated/enums" import { cn } from "@/lib/utils" type RuntimeMode = "workflow" | "autonomous" | "hybrid" type SectionKey = "setup" | "persona" | "capability" | "service" type DirectToolItem = CreateAIAgentPayload["directTools"][number] type DirectToolOption = { value: string label: string group: string subtitle: string description?: string meta: DirectToolItem } const runtimeModes: { value: RuntimeMode title: string description: string }[] = [ { value: "autonomous", title: "自主接待", description: "自主选择知识和工具处理请求,工作流可选。", }, { value: "hybrid", title: "自主接待 + 工作流", description: "自主处理咨询,按需调用一个或多个工作流。", }, { value: "workflow", title: "仅工作流", description: "严格按一个已发布工作流处理会话。", }, ] 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, onCancel, }: { agentId?: number | null onAgentSaved?: () => void onAgentCreated?: (agent: AIAgent) => void onCancel?: () => void }) { const [currentAgentId, setCurrentAgentId] = useState(agentId ?? null) const [activeSection, setActiveSection] = useState("setup") const [agent, setAgent] = useState(null) const [agentRevisions, setAgentRevisions] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [versionDialogOpen, setVersionDialogOpen] = useState(false) const [name, setName] = useState("") const [description, setDescription] = useState("") const [aiConfigId, setAIConfigId] = useState("") const [runtimeMode, setRuntimeMode] = useState("autonomous") 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 [selectedTeamIds, setSelectedTeamIds] = useState([]) const [selectedSkillIds, setSelectedSkillIds] = useState([]) const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]) const [directTools, setDirectTools] = useState([]) const [workflowBindings, setWorkflowBindings] = useState([]) const [aiConfigs, setAIConfigs] = useState([]) const [agentTeams, setAgentTeams] = useState([]) const [skills, setSkills] = useState([]) const [knowledgeBases, setKnowledgeBases] = useState([]) const [toolCatalog, setToolCatalog] = useState([]) const [publishedWorkflows, setPublishedWorkflows] = useState([]) const [teamToAdd, setTeamToAdd] = useState("") useEffect(() => { setCurrentAgentId(agentId ?? null) }, [agentId]) const loadData = useCallback(async () => { setLoading(true) try { const [configs, teams, skillList, knowledgeBaseList, catalog, workflowPage] = await Promise.all([ fetchAIConfigsAll({ modelType: AIModelType.LLM }), fetchAgentTeamsAll(), fetchSkillDefinitionsAll({ status: Status.Ok }), fetchKnowledgeBasesAll({ status: Status.Ok }), fetchMCPCatalog(), fetchAIWorkflows({ limit: 100 }), ]) setAIConfigs(configs ?? []) setAgentTeams(teams ?? []) setSkills(skillList ?? []) setKnowledgeBases(knowledgeBaseList ?? []) setToolCatalog(catalog ?? []) setPublishedWorkflows( (workflowPage.results ?? []).filter((item) => item.publishedVersionId > 0), ) if (!currentAgentId || currentAgentId <= 0) { setAgent(null) setAgentRevisions([]) setName("") setDescription("") setAIConfigId("") setRuntimeMode("autonomous") setServiceMode(String(IMConversationServiceMode.AIFirst)) setSystemPrompt("") setWelcomeMessage("") setReplyTimeoutSeconds("180") setHandoffMode(String(AIAgentHandoffMode.WaitPool)) setFallbackMode(String(AIAgentFallbackMode.NoAnswer)) setFallbackMessage("") setSelectedTeamIds([]) setSelectedSkillIds([]) setSelectedKnowledgeBaseIds([]) setDirectTools([]) setWorkflowBindings([]) return } const [detail, revisions] = await Promise.all([ fetchAIAgent(currentAgentId), fetchAIAgentRevisions(currentAgentId), ]) setAgent(detail) setAgentRevisions(revisions ?? []) setName(detail.name) setDescription(detail.description || "") setAIConfigId(toText(detail.aiConfigId)) setRuntimeMode( detail.runtimeMode === "autonomous" || detail.runtimeMode === "hybrid" ? detail.runtimeMode : "workflow", ) setServiceMode(String(detail.serviceMode || IMConversationServiceMode.AIFirst)) setSystemPrompt(detail.systemPrompt || "") setWelcomeMessage(detail.welcomeMessage || "") setReplyTimeoutSeconds(String(detail.replyTimeoutSeconds ?? 180)) setHandoffMode(String(detail.handoffMode || AIAgentHandoffMode.WaitPool)) setFallbackMode(String(detail.fallbackMode || AIAgentFallbackMode.NoAnswer)) setFallbackMessage(detail.fallbackMessage || "") setSelectedTeamIds((detail.teams ?? []).map((team) => team.id)) setSelectedSkillIds(detail.skillIds ?? []) setSelectedKnowledgeBaseIds(detail.knowledgeBaseIds ?? []) setDirectTools(detail.directTools ?? []) setWorkflowBindings( (detail.workflowBindings ?? []).map( ({ workflowVersionId, toolName, triggerInstruction, priority, enabled }) => ({ workflowVersionId, toolName, triggerInstruction, priority, enabled, }), ), ) } catch (error) { toast.error(error instanceof Error ? error.message : "加载 Agent 配置失败") } finally { setLoading(false) } }, [currentAgentId]) 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: "引导用户补充信息" }, { 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, group: tool.sourceType === "builtin" ? "内置工具" : tool.serverCode, subtitle: tool.toolCode, description: tool.description || undefined, meta: { toolCode: tool.toolCode, serverCode: tool.serverCode, toolName: tool.toolName, title: tool.title || tool.toolName, description: tool.description || "", arguments: undefined, }, })), [toolCatalog], ) const workflowOptions = useMemo( () => publishedWorkflows.map((workflow) => ({ value: String(workflow.publishedVersionId), label: workflow.name, subtitle: `固定版本 #${workflow.publishedVersionId}`, })), [publishedWorkflows], ) 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 } => Boolean(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 setDirectToolSelection(values: string[]) { setDirectTools( values.flatMap((value) => { const option = directToolOptions.find((item) => item.value === value) return option ? [option.meta] : [] }), ) } function setWorkflowSelection(values: string[]) { let selectedVersionIds = values.map(Number).filter((value) => value > 0) if (runtimeMode === "workflow" && selectedVersionIds.length > 1) { const currentIds = workflowBindings.map((binding) => binding.workflowVersionId) const newlySelected = selectedVersionIds.find((id) => !currentIds.includes(id)) selectedVersionIds = [newlySelected ?? selectedVersionIds.at(-1)!] } setWorkflowBindings( selectedVersionIds.flatMap((workflowVersionId, index) => { const current = workflowBindings.find( (binding) => binding.workflowVersionId === workflowVersionId, ) if (current) { return [{ ...current, priority: index + 1, enabled: true }] } const workflow = publishedWorkflows.find( (item) => item.publishedVersionId === workflowVersionId, ) if (!workflow) return [] return [ { workflowVersionId, toolName: workflow.name, triggerInstruction: "", priority: index + 1, enabled: true, }, ] }), ) } function validateForm() { if (!name.trim()) { setActiveSection("setup") toast.error("请填写 Agent 名称") return false } if (!Number(aiConfigId)) { setActiveSection("setup") toast.error("请选择 AI 配置") return false } if (runtimeMode === "hybrid" && workflowBindings.length === 0) { setActiveSection("capability") toast.error("Hybrid 模式至少需要关联一个已发布工作流") return false } if (runtimeMode === "workflow" && workflowBindings.length !== 1) { setActiveSection("capability") toast.error("仅工作流模式必须且只能关联一个已发布工作流") return false } return true } 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: agent?.rolloutPercent || 100, teamIds: uniqueNumbers(selectedTeamIds), handoffMode: Number(handoffMode), fallbackMode: Number(fallbackMode), fallbackMessage: fallbackMessage.trim(), knowledgeBaseIds: uniqueNumbers(selectedKnowledgeBaseIds), skillIds: uniqueNumbers(selectedSkillIds), directTools, workflowBindings, } } async function saveAgentSettings() { if (!validateForm()) return setSaving(true) try { const payload = buildPayload() if (agent) { await updateAIAgent({ id: agent.id, ...payload }) toast.success("Agent 配置已保存") await loadData() } else { const created = await createAIAgent(payload) setCurrentAgentId(created.id) setAgent(created) toast.success("Agent 已创建") onAgentCreated?.(created) } onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "保存 Agent 配置失败") } finally { setSaving(false) } } async function publishAgent() { if (!agent || runtimeMode === "workflow") return if (!validateForm()) return setSaving(true) try { await publishAIAgent(agent.id) await loadData() toast.success("Agent 已发布") onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "发布 Agent 失败") } finally { setSaving(false) } } async function rollbackAgentRevision(revisionId: number) { if (!agent || revisionId <= 0 || revisionId === agent.publishedRevisionId) return setSaving(true) try { await rollbackAIAgent(agent.id, revisionId) toast.success("已回滚到选中的 Agent 版本") await loadData() onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "回滚 Agent 版本失败") } finally { setSaving(false) } } 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 sections: { key: SectionKey title: string icon: ReactNode }[] = [ { key: "setup", title: "身份与运行", icon: }, { key: "persona", title: "角色与回复", icon: }, { key: "capability", title: "知识与能力", icon: }, { key: "service", title: "服务与兜底", icon: }, ] if (loading) { return (
加载中...
) } return (
{/*
*/}

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

{agent?.statusName ? {agent.statusName} : null} {runtimePublished ? "已发布" : agent ? "未发布" : "尚未创建"}
{agent ? ( ) : null}
{activeSection === "setup" ? (
setName(event.target.value)} />