import { useEffect, useMemo, useRef, useState } from 'react'; import { Bot, Boxes, Braces, ChevronDown, ChevronRight, Compass, Crown, Edit, FileCode2, Globe, LineChart, MoreVertical, PenTool, Pencil, PencilIcon, Plus, Search, ShieldCheck, Sparkles, Target, Trash2, Upload, Wand2, Workflow, X, } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { useTheme } from '../../contexts/ThemeContext'; import { useExpertAuthBootstrap } from '../../hooks'; import { knowledgetoolUrl } from '@/hooks/useExpertAuthBootstrap'; import { MessagePlugin } from 'tdesign-react'; interface Skill { id: string; name: string; description: string; category: string; highlight: string; relatedCount: number; version: string; inputCount: number; outputCount: number; isNew?: boolean; isMine: boolean; source?: string; } interface ApiSkill { id: number; name: string; description: string; source?: string; } interface SkillsResponse { items: ApiSkill[]; total: number; page: number; page_size: number; } interface CreateSkillThreadResponse { id: number; title: string; archived: boolean; expert_agent_id: number | null; user_id: string; created_at: string; updated_at: string; } type CategoryKey = 'all' | 'writing' | 'analysis' | 'decision' | 'validation' | 'workflow'; const CATEGORY_LABELS: Record = { all: '全部类型', writing: '编辑类', analysis: '分析类', decision: '决策类', validation: '校验类', workflow: '流程类', }; const categoryOrder: CategoryKey[] = ['all', 'writing', 'analysis', 'decision', 'validation', 'workflow']; const AVATAR_STYLES = [ { Icon: Bot, wrapper: 'from-rose-100 via-red-50 to-white text-red-500', glow: 'shadow-red-100/80', ring: 'ring-red-100', orb: 'bg-red-500/10', }, { Icon: Wand2, wrapper: 'from-violet-100 via-fuchsia-50 to-white text-violet-500', glow: 'shadow-violet-100/80', ring: 'ring-violet-100', orb: 'bg-violet-500/10', }, { Icon: LineChart, wrapper: 'from-cyan-100 via-sky-50 to-white text-sky-500', glow: 'shadow-sky-100/80', ring: 'ring-sky-100', orb: 'bg-sky-500/10', }, { Icon: ShieldCheck, wrapper: 'from-emerald-100 via-green-50 to-white text-emerald-500', glow: 'shadow-emerald-100/80', ring: 'ring-emerald-100', orb: 'bg-emerald-500/10', }, { Icon: Workflow, wrapper: 'from-amber-100 via-yellow-50 to-white text-amber-500', glow: 'shadow-amber-100/80', ring: 'ring-amber-100', orb: 'bg-amber-500/10', }, { Icon: FileCode2, wrapper: 'from-indigo-100 via-blue-50 to-white text-indigo-500', glow: 'shadow-indigo-100/80', ring: 'ring-indigo-100', orb: 'bg-indigo-500/10', }, { Icon: Target, wrapper: 'from-pink-100 via-rose-50 to-white text-pink-500', glow: 'shadow-pink-100/80', ring: 'ring-pink-100', orb: 'bg-pink-500/10', }, { Icon: Compass, wrapper: 'from-orange-100 via-amber-50 to-white text-orange-500', glow: 'shadow-orange-100/80', ring: 'ring-orange-100', orb: 'bg-orange-500/10', }, ]; const hashString = (value: string) => value.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); const inferCategory = (skill: ApiSkill): CategoryKey => { const text = `${skill.name} ${skill.description}`.toLowerCase(); if (/(summary|readme|writer|copy|frontend|narrative|edit|document)/.test(text)) return 'writing'; if (/(analysis|audience|risk|search|insight|research|data)/.test(text)) return 'analysis'; if (/(plan|decision|goal|strategy|logic|outreach)/.test(text)) return 'decision'; if (/(check|validate|review|guard|test|lint)/.test(text)) return 'validation'; return 'workflow'; }; const buildVersion = (id: number) => `v${1 + (id % 3)}.${(id % 8) + 1}`; const buildCount = (id: number, offset: number) => 2 + ((id + offset) % 4); const buildRelatedCount = (id: number) => 3 + (id % 4); const mapApiSkillToUiSkill = (skill: ApiSkill, type: 'official' | 'mine'): Skill => ({ id: String(skill.id), name: skill.name, description: skill.description, category: inferCategory(skill), highlight: type === 'mine' ? '已接入' : '可添加', relatedCount: buildRelatedCount(skill.id), version: buildVersion(skill.id), inputCount: buildCount(skill.id, 1), outputCount: buildCount(skill.id, 2), isNew: skill.id % 3 === 0, isMine: type === 'mine', }); const getAvatarStyle = (skillId: string) => AVATAR_STYLES[hashString(skillId) % AVATAR_STYLES.length]; export default function SkillsWorkshop() { const navigate = useNavigate(); const { authedFetch } = useExpertAuthBootstrap(); const { theme } = useTheme(); const isDarkMode = theme === 'dark'; const [searchTerm, setSearchTerm] = useState(''); const [selectedFilter, setSelectedFilter] = useState<'all' | 'mine'>('all'); const [selectedCategory, setSelectedCategory] = useState('all'); const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false); const [isCreatingSkillThread, setIsCreatingSkillThread] = useState(false); const [openCardMenuId, setOpenCardMenuId] = useState(null); const [editingSkill, setEditingSkill] = useState(null); const [addingSkillId, setAddingSkillId] = useState(null); const [isUploading, setIsUploading] = useState(false); const [isCreatingByUpload, setIsCreatingByUpload] = useState(false); const [isAiEditing, setIsAiEditing] = useState(false); const createMenuRef = useRef(null); const cardMenuRef = useRef(null); const uploadInputRef = useRef(null); const createUploadInputRef = useRef(null); useEffect(() => { let canceled = false; const fetchByType = async (type: 'official' | 'mine') => { const params = new URLSearchParams({ type, keyword: searchTerm.trim(), page: '1', page_size: '100', }); const res = await authedFetch(`${knowledgetoolUrl}/api/v1/skills?${params.toString()}`, { method: 'GET', }); if (!res.ok) { throw new Error(`加载技能失败: ${res.status} ${res.statusText}`); } const data = (await res.json()) as SkillsResponse; return data.items.map((item) => mapApiSkillToUiSkill(item, type)); }; const loadSkills = async () => { setLoading(true); setError(''); try { const list = selectedFilter === 'all' ? await Promise.all([fetchByType('official'), fetchByType('mine')]) : [await fetchByType('mine')]; if (canceled) return; const merged = selectedFilter === 'all' ? [...list[0], ...list[1]] : list[0]; const deduped = Array.from(new Map(merged.map((item) => [item.id, item])).values()); setSkills(deduped); } catch { if (canceled) return; setError('加载技能失败,请稍后重试'); setSkills([]); } finally { if (!canceled) setLoading(false); } }; void loadSkills(); return () => { canceled = true; }; }, [searchTerm, selectedFilter, authedFetch]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; if (createMenuRef.current && !createMenuRef.current.contains(target)) { setIsCreateMenuOpen(false); } if (cardMenuRef.current && !cardMenuRef.current.contains(target)) { setOpenCardMenuId(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); const filteredSkills = useMemo( () => skills.filter((skill) => selectedCategory === 'all' || skill.category === selectedCategory), [selectedCategory, skills], ); const categoryCounts = useMemo( () => categoryOrder.reduce>((acc, category) => { acc[category] = category === 'all' ? skills.length : skills.filter((skill) => skill.category === category).length; return acc; }, {} as Record), [skills], ); const mySkillCount = skills.filter((skill) => skill.isMine).length; const officialSkillCount = skills.filter((skill) => !skill.isMine).length; const handlePublishSkill = async (skillId: string) => { setOpenCardMenuId(null); try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/skills/publish`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ skill_id: parseInt(skillId, 10), }), }); if (!res.ok) { throw new Error(`发布失败: ${res.status} ${res.statusText}`); } } catch (err) { console.error('发布技能失败', err); setError('发布技能失败,请稍后重试'); } }; const handleEditSkill = (skillId: string) => { setOpenCardMenuId(null); const skill = skills.find((item) => item.id === skillId) ?? null; setEditingSkill(skill); }; const handleDeleteSkill = async (skillId: string) => { setOpenCardMenuId(null); try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/skills/mine/${skillId}`, { method: 'DELETE', }); if (!res.ok) { throw new Error(`删除失败: ${res.status} ${res.statusText}`); } setSkills((prev) => prev.filter((item) => item.id !== skillId)); MessagePlugin.success('操作成功'); } catch (err) { console.error('删除技能失败', err); setError('删除技能失败,请稍后重试'); } }; const handleUploadSkillFile = async (file: File, skillId: string) => { setIsUploading(true); try { const formData = new FormData(); formData.append('file', file); const res = await authedFetch(`${knowledgetoolUrl}/api/v1/skills/${skillId}/upload`, { method: 'PUT', body: formData, }); if (!res.ok) { throw new Error(`上传失败: ${res.status} ${res.statusText}`); } setEditingSkill(null); } catch (err) { console.error('上传技能失败', err); setError('上传技能失败,请稍后重试'); } finally { setIsUploading(false); if (uploadInputRef.current) uploadInputRef.current.value = ''; } }; const handleAddOfficialSkill = async (skillId: string) => { if (addingSkillId) return; setAddingSkillId(skillId); try { const res = await authedFetch(`${knowledgetoolUrl}/api/v1/skills/mine/add`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ official_skill_id: parseInt(skillId, 10) }), }); if (!res.ok) { throw new Error(`添加失败: ${res.status} ${res.statusText}`); } setSkills((prev) => prev.map((item) => (item.id === skillId ? { ...item, isMine: true, highlight: '已接入' } : item))); MessagePlugin.success('添加成功'); } catch (err) { console.error('添加技能失败', err); setError('添加技能失败,请稍后重试'); } finally { setAddingSkillId(null); } }; const handleAiEditSkill = async (skill: Skill) => { if (isAiEditing) return; setIsAiEditing(true); try { const threadRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agent-chat/threads`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: `编辑技能:${skill.name}`, archived: true, is_real_chat: true, }), }); if (!threadRes.ok) { throw new Error(`创建会话失败: ${threadRes.status} ${threadRes.statusText}`); } const threadData = (await threadRes.json()) as CreateSkillThreadResponse; const threadId = threadData?.id; if (!threadId) throw new Error('创建会话成功,但未返回会话 id'); const copyRes = await authedFetch( `${knowledgetoolUrl}/api/v1/expert-agent-chat/threads/${threadId}/copy-skill/${skill.id}`, { method: 'POST' }, ); if (!copyRes.ok) { throw new Error(`复制技能失败: ${copyRes.status} ${copyRes.statusText}`); } setEditingSkill(null); localStorage.setItem('skill_creator_thread_id', String(threadId)); navigate(`/skill-creator?id=${threadId}`); } catch (err) { console.error('AI 辅助编辑失败', err); setError('AI 辅助编辑失败,请稍后重试'); } finally { setIsAiEditing(false); } }; const handleCreateSkillByUpload = async (file: File) => { setIsCreatingByUpload(true); setIsCreateMenuOpen(false); try { const formData = new FormData(); formData.append('file', file); const res = await authedFetch(`${knowledgetoolUrl}/api/v1/skills/upload`, { method: 'POST', body: formData, }); if (!res.ok) { throw new Error(`上传失败: ${res.status} ${res.statusText}`); } } catch (err) { console.error('上传技能失败', err); setError('上传技能失败,请稍后重试'); } finally { setIsCreatingByUpload(false); if (createUploadInputRef.current) createUploadInputRef.current.value = ''; } }; const handleCreateSkillByAgent = async () => { if (isCreatingSkillThread) return; setIsCreatingSkillThread(true); setIsCreateMenuOpen(false); try { const threadRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agent-chat/threads`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: '新建技能', archived: true, is_real_chat: true, }), }); if (!threadRes.ok) { throw new Error(`创建技能会话失败: ${threadRes.status} ${threadRes.statusText}`); } const threadData = (await threadRes.json()) as CreateSkillThreadResponse; const threadId = threadData?.id; if (!threadId) { throw new Error('创建技能会话成功,但未返回会话 id'); } localStorage.setItem('skill_creator_thread_id', String(threadId)); navigate(`/qa-center/skill-creator?id=${threadId}`); } catch (createError) { console.error('创建技能会话失败', createError); setError('创建技能会话失败,请稍后重试'); } finally { setIsCreatingSkillThread(false); } }; return (
{editingSkill && (
{ if (event.target === event.currentTarget) setEditingSkill(null); }} >

编辑技能

选择编辑方式:{editingSkill.name}

{ const file = event.target.files?.[0]; if (file && editingSkill) { void handleUploadSkillFile(file, editingSkill.id); } }} />
)}

技能工坊

统一管理系统中的技能资产,支持发现、接入、发布与编辑。

{isCreateMenuOpen && (
{ const file = event.target.files?.[0]; if (file) void handleCreateSkillByUpload(file); }} />
)}
setSearchTerm(event.target.value)} className={`min-w-0 flex-1 bg-transparent text-sm outline-none ${isDarkMode ? 'text-slate-100 placeholder:text-slate-500' : 'text-gray-800 placeholder:text-[#9ca3af]'}`} />
{/*
按类型筛选: {categoryOrder.slice(1).map((category) => { const active = selectedCategory === category; return ( ); })}
*/}
{loading ? (
正在加载技能...
) : error ? (

加载失败

{error}

) : filteredSkills.length === 0 ? (

未找到匹配的技能

尝试调整搜索词或切换筛选条件。

) : (
{filteredSkills.map((skill) => { const avatar = getAvatarStyle(skill.id); const AvatarIcon = avatar.Icon; const categoryLabel = CATEGORY_LABELS[skill.category as CategoryKey] ?? '流程类'; return (
{!skill.isMine && selectedFilter === 'all' && (
)} {skill.isMine && (
{openCardMenuId === skill.id && (
)}
)}

{skill.name}

{ skill.isMine ? '我的' : '官方' }

{skill.description}

); })}
)}
); }