"use client" import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react" import { ArrowDownIcon, ArrowUpIcon, BotMessageSquareIcon, BrainCircuitIcon, CheckCircle2Icon, DatabaseIcon, GitBranchIcon, LifeBuoyIcon, PlugIcon, SaveIcon, SendIcon, SettingsIcon, ShieldCheckIcon, Trash2Icon, } from "lucide-react" import { toast } from "sonner" import { OptionCombobox } from "@/components/option-combobox" 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 { Textarea } from "@/components/ui/textarea" import { fetchAIAgent, fetchAIAgentWorkflow, fetchAIConfigsAll, fetchAIWorkflowNodeSpecs, fetchAgentTeamsAll, fetchKnowledgeBasesAll, fetchMCPCatalog, fetchSkillDefinitionsAll, publishAIAgentWorkflow, saveAIAgentWorkflow, updateAIAgent, validateAIWorkflow, type AIAgent, type AIConfig, type AIWorkflow, type AIWorkflowDefinition, type AIWorkflowNodeSpec, type AIWorkflowValidationResult, 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" | "model" | "knowledge" | "skills" | "tools" | "workflow" | "handoff" | "publish" const emptyDefinition: AIWorkflowDefinition = { schemaVersion: 1, entryNodeId: "start_1", nodes: [ { id: "start_1", type: "start", name: "Start", position: { x: 0, y: 80 }, config: {}, }, { id: "end_1", type: "end", name: "End", position: { x: 360, y: 80 }, config: {}, }, ], edges: [{ id: "edge_start_end", source: "start_1", target: "end_1" }], } 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, }: { agentId: number onAgentSaved?: () => void }) { const [activeSection, setActiveSection] = useState("basic") const [agent, setAgent] = useState(null) const [workflow, setWorkflow] = useState(null) const [nodeSpecs, setNodeSpecs] = useState([]) const [validation, setValidation] = useState(null) const [loading, setLoading] = useState(true) const [savingAgent, setSavingAgent] = useState(false) const [savingWorkflow, setSavingWorkflow] = 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 [workflowName, setWorkflowName] = useState("") const [workflowDescription, setWorkflowDescription] = useState("") const [definition, setDefinition] = useState(emptyDefinition) 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("") const editorKey = useMemo( () => `${workflow?.id ?? "new"}-${workflow?.updatedAt ?? ""}`, [workflow?.id, workflow?.updatedAt] ) const loadData = useCallback(async () => { if (!Number.isFinite(agentId) || agentId <= 0) { setLoading(false) return } setLoading(true) try { const [ agentDetail, workflowDetail, specs, configs, bases, teams, skillList, catalog, ] = await Promise.all([ fetchAIAgent(agentId), fetchAIAgentWorkflow(agentId), fetchAIWorkflowNodeSpecs(), fetchAIConfigsAll({ modelType: AIModelType.LLM }), fetchKnowledgeBasesAll({ status: Status.Ok }), fetchAgentTeamsAll(), fetchSkillDefinitionsAll({ status: Status.Ok }), fetchMCPCatalog(), ]) setAgent(agentDetail) setWorkflow(workflowDetail) setNodeSpecs(specs ?? []) setAIConfigs(configs ?? []) setKnowledgeBases(bases ?? []) setAgentTeams(teams ?? []) setSkills(skillList ?? []) setToolCatalog(catalog ?? []) 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 ?? []) setWorkflowName(workflowDetail.name || `${agentDetail.name} 会话流程`) setWorkflowDescription(workflowDetail.description || "") setDefinition(workflowDetail.draftDefinition ?? emptyDefinition) setValidation(null) } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to load Agent config") } finally { setLoading(false) } }, [agentId]) 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, graphTools: [], } } async function saveAgentSettings() { if (!agent) return setSavingAgent(true) try { const payload = buildPayload() await updateAIAgent({ id: agent.id, ...payload }) toast.success("Agent config saved") await loadData() onAgentSaved?.() } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to save Agent config") } finally { setSavingAgent(false) } } async function saveWorkflowDraft() { setSavingWorkflow(true) try { const saved = await saveAIAgentWorkflow({ agentId, name: workflowName, description: workflowDescription, definition, }) setWorkflow(saved) 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) setValidation(result) 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 publishWorkflow() { setSavingWorkflow(true) try { await saveAIAgentWorkflow({ agentId, name: workflowName, description: workflowDescription, definition, }) const version = await publishAIAgentWorkflow(agentId, definition) toast.success(`Published version ${version.version}`) await loadData() 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: "model", title: "模型与 Prompt", icon: }, { key: "knowledge", title: "知识库", icon: }, { key: "skills", title: "Skills", icon: }, { key: "tools", title: "工具能力", icon: }, { key: "workflow", title: "会话流程", icon: }, { key: "handoff", title: "转人工与兜底", icon: }, { key: "publish", title: "发布状态", icon: }, ] const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions) const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions) const selectedSkillOptions = selectedOptions(selectedSkillIds, skillOptions) return (

{agent?.name ?? "AI Agent 配置"}

{agent?.statusName ? {agent.statusName} : null} {agent?.workflowVersionId ? 已发布流程 : 草稿流程}
{loading ? (
加载中...
) : (
{activeSection === "basic" ? (
setName(event.target.value)} />