"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 SectionKey = "setup" | "persona" | "capability" | "service" type MCPToolItem = CreateAIAgentPayload["mcpTools"][number] type MCPToolOption = { value: string label: string group: string subtitle: string description?: string meta: MCPToolItem } 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))) } 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 [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 [mcpTools, setMCPTools] = 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("") setServiceMode(String(IMConversationServiceMode.AIFirst)) setSystemPrompt("") setWelcomeMessage("") setReplyTimeoutSeconds("180") setHandoffMode(String(AIAgentHandoffMode.WaitPool)) setFallbackMode(String(AIAgentFallbackMode.NoAnswer)) setFallbackMessage("") setSelectedTeamIds([]) setSelectedSkillIds([]) setSelectedKnowledgeBaseIds([]) setMCPTools([]) 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)) 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 ?? []) setMCPTools(detail.mcpTools ?? []) 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 mcpToolOptions = useMemo( () => toolCatalog .filter((tool) => !tool.autoInjected && tool.sourceType === "mcp") .map((tool) => ({ value: tool.toolCode, label: tool.title || tool.toolName, group: 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 || "", riskLevel: "read", requireConfirmation: false, 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 setMCPToolSelection(values: string[]) { setMCPTools( values.flatMap((value) => { const current = mcpTools.find((item) => item.toolCode === value) if (current) return [current] const option = mcpToolOptions.find((item) => item.value === value) return option ? [option.meta] : [] }), ) } function setWorkflowSelection(values: string[]) { const selectedVersionIds = values.map(Number).filter((value) => value > 0) 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 } return true } 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), rolloutPercent: agent?.rolloutPercent || 100, teamIds: uniqueNumbers(selectedTeamIds), handoffMode: Number(handoffMode), fallbackMode: Number(fallbackMode), fallbackMessage: fallbackMessage.trim(), knowledgeBaseIds: uniqueNumbers(selectedKnowledgeBaseIds), skillIds: uniqueNumbers(selectedSkillIds), mcpTools, 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) return if (!validateForm()) return setSaving(true) try { const payload = buildPayload() await updateAIAgent({ id: agent.id, ...payload }) 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 agentPublished = (agent?.publishedRevisionId ?? 0) > 0 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} {agentPublished ? "已发布" : agent ? "未发布" : "尚未创建"}
{agent ? ( ) : null}
{activeSection === "setup" ? (
setName(event.target.value)} />