import { useEffect, useRef, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ArrowLeft, Bot, Copy, Download, ImagePlus, Paperclip, Pencil, Save, Sparkles, Wrench, Flower2, CheckCircle2, X, ChevronDown, Edit, } from 'lucide-react'; import AgentConversationList, { type ConversationMessage } from './AgentConversationList'; import ChatInputComposer from './ChatInputComposer'; import { Input, Select, Tabs, Textarea, Dialog, MessagePlugin } from 'tdesign-react'; import { useTheme } from '../../contexts/ThemeContext'; import { useExpertAuthBootstrap } from '../../hooks'; import { knowledgetoolUrl } from '@/hooks/useExpertAuthBootstrap'; interface AgentExpertEditPageProps { agentId?: string | null; } export default function AgentExpertEditPage({ agentId: propAgentId }: AgentExpertEditPageProps) { const { theme } = useTheme(); const isDark = theme === 'dark'; const { llmList, authedFetch } = useExpertAuthBootstrap(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const agentId = propAgentId || searchParams.get('id') || '12'; const [tab, setTab] = useState('edit'); const [toolDialogVisible, setToolDialogVisible] = useState(false); const [skillDialogVisible, setSkillDialogVisible] = useState(false); const [selectedTools, setSelectedTools] = useState([]); const [pendingTools, setPendingTools] = useState([]); const [boundToolIdMap, setBoundToolIdMap] = useState>({}); const [selectedSkills, setSelectedSkills] = useState([]); const [skillKeyword, setSkillKeyword] = useState(''); const [skillOptions, setSkillOptions] = useState<{ id: string; name: string; desc: string }[]>([]); const [boundSkillMap, setBoundSkillMap] = useState>({}); const [isSkillLoading, setIsSkillLoading] = useState(false); const [bindingSkillIds, setBindingSkillIds] = useState([]); const [unbindingSkillIds, setUnbindingSkillIds] = useState([]); const [isToolSubmitting, setIsToolSubmitting] = useState(false); const [isSaving, setIsSaving] = useState(false); const [toolOptions, setToolOptions] = useState<{ id: string; name: string; desc: string }[]>([]); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [threadId, setThreadId] = useState(null); const [isStreaming, setIsStreaming] = useState(false); const thinkingStartRef = useRef(0); const scrollContainerRef = useRef(null); const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); const [showScrollButton, setShowScrollButton] = useState(false); const router = useNavigate(); const scrollToBottom = (smooth = true) => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, behavior: smooth ? 'smooth' : 'auto', }); } }; const handleScroll = (e: React.UIEvent) => { const container = e.currentTarget; const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50; setShowScrollButton(!isAtBottom && messages.length > 0); if (!isAtBottom) { setAutoScrollEnabled(false); } }; useEffect(() => { if (autoScrollEnabled && messages.length > 0) { const timer = setTimeout(() => scrollToBottom(false), 100); return () => clearTimeout(timer); } }, [messages, autoScrollEnabled]); const ensureThreadId = async () => { if (threadId) return threadId; const threadRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agent-chat/threads/no_real/${agentId}`, { method: 'GET', }); if (!threadRes.ok) { throw new Error(`获取会话失败: ${threadRes.status}`); } const threadData = (await threadRes.json()) as { id: number }; setThreadId(threadData.id); return threadData.id; }; const sendMessage = async () => { const text = inputValue.trim(); if (!text || isStreaming) return; setInputValue(''); setIsStreaming(true); const userMsg: ConversationMessage = { id: `user-${Date.now()}`, role: 'user', content: text, }; setMessages((prev) => [...prev, userMsg]); try { // 1. GET or reuse thread let tid = threadId; if (!tid) { const threadRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agent-chat/threads/no_real/${agentId}`, { method: 'GET', }); if (!threadRes.ok) { console.error('创建会话失败:', threadRes.status); return; } const threadData = (await threadRes.json()) as { id: number }; tid = threadData.id; setThreadId(tid); } // 2. POST user message const msgRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agent-chat/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: tid, role: 'user', content: { type: 'text', text } }), }); if (!msgRes.ok) { console.error('发送消息失败:', msgRes.status); return; } // 3. Stream assistant response const assistantId = `assistant-${Date.now()}`; setMessages((prev) => [ ...prev, { id: assistantId, role: 'assistant', content: '', steps: [], thinkings: [], toolCalls: [] }, ]); const streamRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agent-chat/run/stream/help_expert`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: tid, expert_agent_id: Number(agentId), user_query: text, messages: [] }), }); if (!streamRes.ok || !streamRes.body) { console.error('流式接口失败:', streamRes.status); return; } const reader = streamRes.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const raw = line.slice(6).trim(); if (raw === '[DONE]') break; let event: { type: string; data?: { id: string; title: string; status: string; items?: string[] }; id?: string; delta?: string } | null = null; try { event = JSON.parse(raw) as typeof event; } catch { continue; } if (!event) continue; setMessages((prev) => prev.map((msg) => { if (msg.id !== assistantId) return msg; switch (event!.type) { case 'data-thinking-step': { const step = event!.data!; const newStep = { text: step.title, subText: step.items?.[0]?.replace('正在处理: ', '') ?? undefined, status: (step.status === 'completed' ? 'done' : 'processing') as 'done' | 'processing', }; const steps = [...(msg.steps ?? [])]; const idx = steps.findIndex((s) => s.text === step.title); if (idx >= 0) steps[idx] = newStep; else steps.push(newStep); return { ...msg, steps }; } case 'reasoning-start': { thinkingStartRef.current = Date.now(); return { ...msg, thinkings: [...(msg.thinkings ?? []), { duration: 0, content: '' }] }; } case 'reasoning-delta': { const thinkings = [...(msg.thinkings ?? [])]; if (thinkings.length === 0) { thinkings.push({ duration: 0, content: event!.delta ?? '' }); } else { const last = thinkings[thinkings.length - 1]; thinkings[thinkings.length - 1] = { ...last, content: last.content + (event!.delta ?? '') }; } return { ...msg, thinkings }; } case 'reasoning-end': { const duration = (Date.now() - thinkingStartRef.current) / 1000; const thinkings = [...(msg.thinkings ?? [])]; if (thinkings.length > 0) { thinkings[thinkings.length - 1] = { ...thinkings[thinkings.length - 1], duration }; } return { ...msg, thinkings }; } case 'text-delta': { return { ...msg, content: msg.content + (event!.delta ?? '') }; } default: return msg; } }), ); } } } catch (error) { console.error('对话异常:', error); } finally { setIsStreaming(false); } }; const handleUploadFile = async (file: File) => { try { const tid = await ensureThreadId(); const formData = new FormData(); formData.append('file', file); const response = await authedFetch( `${knowledgetoolUrl}/api/v1/expert-agent-chat/threads/${tid}/files/upload`, { method: 'POST', body: formData, }, ); if (!response.ok) { throw new Error(`上传失败: ${response.status} ${response.statusText}`); } } catch (error) { console.error('上传专家编辑文件失败:', error); } }; const [formValues, setFormValues] = useState({ name: '未命名专家', modelName: 'Qwen3.5-plus', description: '', systemPrompt: '你是一个专业的助手,能够回答用户的问题。', }); const modelOptions = llmList.map((item) => ({ label: item.name, value: item.id })); useEffect(() => { const fetchExpertDetail = async () => { try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}`, { method: 'GET', }); if (!res.ok) { console.error('查询专家详情失败:', res.status, res.statusText); return; } const data: unknown = await res.json(); const detail = data as { name?: string; description?: string | null; system_prompt?: string | null; model_name?: string | null; }; setFormValues((prev) => ({ ...prev, name: detail.name || '', description: detail.description || '', systemPrompt: detail.system_prompt || '', modelName: detail.model_name || prev.modelName, })); } catch (error) { console.error('查询专家详情异常:', error); } }; void fetchExpertDetail(); }, [authedFetch, agentId]); useEffect(() => { const fetchBuiltinTools = async () => { try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/tools/builtin`, { method: 'GET', }); if (!res.ok) { console.error('builtin tools接口调用失败:', res.status, res.statusText); setToolOptions([]); return; } const data: unknown = await res.json(); const list = Array.isArray(data) ? (data as { name: string; description: string }[]).map((item) => ({ id: item.name, name: item.name, desc: item.description, })) : []; setToolOptions(list); } catch (error) { console.error('builtin tools接口异常:', error); setToolOptions([]); } }; void fetchBuiltinTools(); }, [authedFetch]); useEffect(() => { const fetchSkills = async () => { setIsSkillLoading(true); try { const keyword = encodeURIComponent(skillKeyword.trim()); const res = await authedFetch(`${knowledgetoolUrl}/api/v1/skills?type=all&keyword=${keyword}&page=1&page_size=100`, { method: 'GET', }); if (!res.ok) { console.error('skills接口调用失败:', res.status, res.statusText); setSkillOptions([]); return; } const data: unknown = await res.json(); const items = data && typeof data === 'object' && Array.isArray((data as { items?: unknown[] }).items) ? ((data as { items: Array<{ id: number; name: string; description: string }> }).items || []) : []; setSkillOptions( items.map((item) => ({ id: String(item.id), name: item.name, desc: item.description, })), ); } catch (error) { console.error('skills接口异常:', error); setSkillOptions([]); } finally { setIsSkillLoading(false); } }; void fetchSkills(); }, [authedFetch, skillKeyword]); useEffect(() => { const fetchBoundSkills = async () => { try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/skills`, { method: 'GET', }); if (!res.ok) { console.error('查询已绑定技能失败:', res.status, res.statusText); return; } const data: unknown = await res.json(); const list = Array.isArray(data) ? (data as Array<{ id: number; name: string; description: string }>).map((item) => ({ id: String(item.id), name: item.name, desc: item.description, })) : []; setSelectedSkills(list.map((item) => item.id)); setBoundSkillMap( list.reduce>((acc, item) => { acc[item.id] = { name: item.name, desc: item.desc }; return acc; }, {}), ); } catch (error) { console.error('查询已绑定技能异常:', error); } }; void fetchBoundSkills(); }, [authedFetch, agentId]); useEffect(() => { const fetchBoundTools = async () => { try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/tools`, { method: 'GET', }); if (!res.ok) { console.error('查询已绑定内置工具失败:', res.status, res.statusText); return; } const data: unknown = await res.json(); const list = Array.isArray(data) ? (data as Array<{ id: number; name: string }>).map((item) => ({ id: String(item.id), name: item.name, })) : []; setSelectedTools(list.map((item) => item.name)); setBoundToolIdMap( list.reduce>((acc, item) => { acc[item.name] = item.id; return acc; }, {}), ); } catch (error) { console.error('查询已绑定内置工具异常:', error); } }; void fetchBoundTools(); }, [authedFetch, agentId]); const refreshBoundTools = async () => { try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/tools`, { method: 'GET', }); if (!res.ok) { console.error('刷新已绑定内置工具失败:', res.status, res.statusText); return; } const data: unknown = await res.json(); const list = Array.isArray(data) ? (data as Array<{ id: number; name: string }>).map((item) => ({ id: String(item.id), name: item.name })) : []; setSelectedTools(list.map((item) => item.name)); setBoundToolIdMap( list.reduce>((acc, item) => { acc[item.name] = item.id; return acc; }, {}), ); } catch (error) { console.error('刷新已绑定内置工具异常:', error); } }; const unbindTool = async (toolName: string) => { const toolId = boundToolIdMap[toolName]; if (!toolId) return; try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/tools/${toolId}`, { method: 'DELETE', }); if (!res.ok) { console.error('删除内置工具失败:', res.status, res.statusText); return; } await refreshBoundTools(); } catch (error) { console.error('删除内置工具异常:', error); } }; const bindSelectedTools = async () => { setIsToolSubmitting(true); try { const toAdd = pendingTools.filter((name) => !selectedTools.includes(name)); for (const toolName of toAdd) { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/tools`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tool_name: toolName }), }); if (!res.ok) { console.error('绑定内置工具失败:', res.status, res.statusText); return; } } await refreshBoundTools(); setToolDialogVisible(false); } catch (error) { console.error('绑定内置工具异常:', error); } finally { setIsToolSubmitting(false); } }; const bindSkill = async (skillId: string) => { if (bindingSkillIds.includes(skillId)) return; setBindingSkillIds((prev) => [...prev, skillId]); try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/skills`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ skill_id: Number(skillId) }), }); if (!res.ok) { console.error('绑定技能失败:', res.status, res.statusText); return; } const refreshRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/skills`, { method: 'GET', }); if (!refreshRes.ok) { console.error('刷新已绑定技能失败:', refreshRes.status, refreshRes.statusText); return; } const refreshData: unknown = await refreshRes.json(); const refreshList = Array.isArray(refreshData) ? (refreshData as Array<{ id: number; name: string; description: string }>).map((item) => ({ id: String(item.id), name: item.name, desc: item.description, })) : []; setSelectedSkills(refreshList.map((item) => item.id)); setBoundSkillMap( refreshList.reduce>((acc, item) => { acc[item.id] = { name: item.name, desc: item.desc }; return acc; }, {}), ); } catch (error) { console.error('绑定技能异常:', error); } finally { setBindingSkillIds((prev) => prev.filter((id) => id !== skillId)); } }; const unbindSkill = async (skillId: string) => { if (unbindingSkillIds.includes(skillId)) return; setUnbindingSkillIds((prev) => [...prev, skillId]); try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/skills/${skillId}`, { method: 'DELETE', }); if (!res.ok) { console.error('解除绑定技能失败:', res.status, res.statusText); return; } const refreshRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}/skills`, { method: 'GET', }); if (!refreshRes.ok) { console.error('刷新已绑定技能失败:', refreshRes.status, refreshRes.statusText); return; } const refreshData: unknown = await refreshRes.json(); const refreshList = Array.isArray(refreshData) ? (refreshData as Array<{ id: number; name: string; description: string }>).map((item) => ({ id: String(item.id), name: item.name, desc: item.description, })) : []; setSelectedSkills(refreshList.map((item) => item.id)); setBoundSkillMap( refreshList.reduce>((acc, item) => { acc[item.id] = { name: item.name, desc: item.desc }; return acc; }, {}), ); } catch (error) { console.error('解除绑定技能异常:', error); } finally { setUnbindingSkillIds((prev) => prev.filter((id) => id !== skillId)); } }; const saveExpert = async () => { setIsSaving(true); try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${agentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: formValues.name, avatar_url: null, description: formValues.description, system_prompt: formValues.systemPrompt, model_name: formValues.modelName, }), }); if (!res.ok) { console.error('保存专家失败:', res.status, res.statusText); return; } MessagePlugin.success('保存成功'); console.log('保存专家成功'); } catch (error) { console.error('保存专家异常:', error); } finally { setIsSaving(false); } }; return ( <>

编辑专家

修改 未命名专家 的配置

{messages.length === 0 ? (

你好,我是 {formValues.name || '未命名专家'}

创建你的专属 AI 助手

想和你聊些什么呀
告诉我你的想法 给我一些建议 帮我分析问题
) : ( <> )}
{showScrollButton && ( )} void sendMessage()} onUploadFile={(file) => void handleUploadFile(file)} isSending={isStreaming} isDarkMode={isDark} showBorderTop placeholder={isStreaming ? '正在思考中...' : '输入你的问题...'} trailingActions={ } className={isStreaming ? 'ring-1 ring-violet-300/40' : ''} />
{/*
setTab(String(v))} size="medium" className="w-full">
*/} {tab === 'preview' ? ( <>

你好,我是 {formValues.name || '未命名专家'}

创建你的专属 AI 助手

想和你聊些什么呀
告诉我你的想法 给我一些建议 帮我分析问题
} /> ) : (

基本信息

setFormValues((prev) => ({ ...prev, name: String(value) }))} />