/** * Skills 管理中心 - 列表页面 */ import { useEffect, useState } from 'react' import { Card, Table, Tag, Button, Space, Input, message, Tabs, Drawer, Modal, Form, Select, Popconfirm, Badge, Divider, Tooltip, Switch, Empty, Spin, Alert, Progress, List, Typography } from 'antd' import { SkillCreate } from '@/components/SkillCreate' import { PlusOutlined, EyeOutlined, EditOutlined, PlayCircleOutlined, SearchOutlined, CopyOutlined, DeleteOutlined, AppstoreOutlined, SettingOutlined, CheckCircleOutlined, LockOutlined, RobotOutlined, EditFilled, ReloadOutlined, ClockCircleOutlined, CloseCircleOutlined, CheckOutlined } from '@ant-design/icons' import { useNavigate, useSearchParams, useLocation } from 'react-router-dom' import { useSkillStore, Skill } from '@/stores/skillStore' import { skillService, SkillDraft } from '@/services/skillService' import { taskService } from '@/services/taskService' import type { ColumnsType } from 'antd/es/table' import { TablePaginationConfig } from 'antd/es/table' import dayjs from 'dayjs' const { Search, TextArea } = Input const { TabPane } = Tabs const { Option } = Select // Keep Input component available const InputComponent = Input interface SkillFormData { id: string name: string category: string behavior_guide: string tags: string[] config?: Record } // AI 创建模式 type CreateMode = 'ai' | 'manual' | 'template' export const SkillManagement = () => { const navigate = useNavigate() const [searchParams] = useSearchParams() const location = useLocation() const { skills, builtinSkills, userSkills, loading, fetchSkills, testSkill } = useSkillStore() // List view states const [searchText, setSearchText] = useState('') const [activeTab, setActiveTab] = useState('all') const [viewMode, setViewMode] = useState<'list' | 'grid'>('list') const [selectedCategories, setSelectedCategories] = useState([]) // Detail view states const [detailDrawerOpen, setDetailDrawerOpen] = useState(false) const [selectedSkill, setSelectedSkill] = useState(null) // Test states const [testModalOpen, setTestModalOpen] = useState(false) const [testInput, setTestInput] = useState('') const [testResult, setTestResult] = useState('') const [testing, setTesting] = useState(false) // CRUD states const [editModalOpen, setEditModalOpen] = useState(false) const [createModalOpen, setCreateModalOpen] = useState(false) const [editingSkill, setEditingSkill] = useState(null) const [form] = Form.useForm() const [submitting, setSubmitting] = useState(false) // AI 创建状态 const [createMode, setCreateMode] = useState('ai') const [aiIdea, setAiIdea] = useState('') const [aiCategory, setAiCategory] = useState() const [generating, setGenerating] = useState(false) const [generatedDraft, setGeneratedDraft] = useState(null) // 技能生成任务状态 const [skillGenerationTasks, setSkillGenerationTasks] = useState([]) const [loadingTasks, setLoadingTasks] = useState(false) const [optimizing, setOptimizing] = useState(false) const [optimizeRequirements, setOptimizeRequirements] = useState('') // 模板初始化状态 const [templateSkillName, setTemplateSkillName] = useState('') const [templateOutputPath, setTemplateOutputPath] = useState('') const [initializing, setInitializing] = useState(false) const [initResult, setInitResult] = useState(null) // 向导状态 const [wizardVisible, setWizardVisible] = useState(false) // 跳转回退信息(从外部跳转过来时使用) const [redirectState, setRedirectState] = useState<{ path: string; activeTab?: string; reviewSubTab?: string } | null>(null) useEffect(() => { fetchSkills() // 轮询技能生成任务 - 使用递归 setTimeout 而不是 setInterval // 这样可以确保前一个请求完成后才开始下一个请求 let isMounted = true const pollTasks = async () => { if (!isMounted) return try { await fetchSkillGenerationTasks() } catch (error) { console.error('Error polling tasks:', error) } // 等待 3 秒后再次轮询 if (isMounted) { setTimeout(pollTasks, 3000) } } // 启动轮询 pollTasks() return () => { isMounted = false } }, []) // 处理URL参数和location.state - 支持通过URL参数打开编辑或创建 useEffect(() => { // 从 location.state 读取 redirect 信息 const state = location.state as any if (state?.redirect) { setRedirectState({ path: state.redirect, activeTab: state.activeTab, reviewSubTab: state.reviewSubTab }) // 清除 state 避免重复处理 location.state = null } // 处理编辑参数 - 使用新的 SkillCreate 向导组件 const editSkillId = searchParams.get('edit') if (editSkillId) { const skillToEdit = skills.find(s => s.id === editSkillId) if (skillToEdit) { setEditingSkill(skillToEdit) setWizardVisible(true) // 清除URL参数 searchParams.delete('edit') navigate(`/skills?${searchParams.toString()}`, { replace: true }) return } else { // Skill 不在本地列表中,尝试从服务器直接获取 // 这允许编辑在 review 配置中引用但不在主列表中的技能 const fetchSkillForEdit = async () => { try { const result = await skillService.getSkillWithReferences(editSkillId, false) if (result?.skill) { setEditingSkill(result.skill) setWizardVisible(true) } else { message.error(`Skill ID "${editSkillId}" 不存在`) } } catch (error) { console.error('Failed to fetch skill for edit:', error) message.error(`无法加载 Skill "${editSkillId}": ${(error as Error).message}`) } finally { // 清除URL参数 searchParams.delete('edit') navigate(`/skills?${searchParams.toString()}`, { replace: true }) } } fetchSkillForEdit() return } } // 处理创建参数 - 使用新的 SkillCreate 向导组件 const shouldCreate = searchParams.get('create') if (shouldCreate === 'true') { // 确保重置 editingSkill,避免误用之前的编辑状态 setEditingSkill(null) setWizardVisible(true) // 清除URL参数 searchParams.delete('create') navigate(`/skills?${searchParams.toString()}`, { replace: true }) } }, [skills, searchParams, navigate, location]) // 获取技能生成任务 const fetchSkillGenerationTasks = async () => { try { const tasks = await taskService.listTasks({ type: 'generate_skill', status: 'running' }) setSkillGenerationTasks(tasks) // 如果有任务完成,刷新技能列表 const completedTasks = tasks.filter(task => task.status === 'completed') if (completedTasks.length > 0) { fetchSkills() } } catch (error) { console.error('Failed to fetch skill generation tasks:', error) } } // Get unique categories const categories = Array.from(new Set(skills.map(s => s.category))) // Filter skills const filteredSkills = skills.filter(skill => { const matchSearch = skill.name.toLowerCase().includes(searchText.toLowerCase()) || skill.category.toLowerCase().includes(searchText.toLowerCase()) || skill.id.toLowerCase().includes(searchText.toLowerCase()) const matchTab = activeTab === 'all' || (activeTab === 'builtin' && skill.type === 'builtin') || (activeTab === 'user' && skill.type === 'user') const matchCategory = selectedCategories.length === 0 || selectedCategories.includes(skill.category) return matchSearch && matchTab && matchCategory }) // Handlers const handleView = (skill: Skill) => { setSelectedSkill(skill) setDetailDrawerOpen(true) } const handleTest = (skill: Skill) => { setSelectedSkill(skill) setTestInput('') setTestResult('') setTestModalOpen(true) } const handleRunTest = async () => { if (!selectedSkill) return setTesting(true) setTestResult('') try { const result = await testSkill(selectedSkill.id, testInput || '请创作一段示例内容') setTestResult(result) message.success('测试成功!') } catch (error) { message.error(`测试失败: ${(error as Error).message}`) } finally { setTesting(false) } } const handleEdit = (skill: Skill) => { if (skill.type === 'builtin') { message.warning('内置 Skill 不允许编辑') return } // 使用新的 SkillCreate 组件进行编辑 setEditingSkill(skill) setWizardVisible(true) } const handleCopy = (skill: Skill) => { form.setFieldsValue({ id: `${skill.id}-copy-${Date.now()}`, name: `${skill.name} (副本)`, category: skill.category, behavior_guide: skill.behavior_guide, tags: skill.tags || [] }) setEditingSkill(null) setCreateModalOpen(true) message.info('已复制 Skill 内容,请修改后保存') } const handleDelete = async (skill: Skill) => { if (skill.type === 'builtin') { message.warning('内置 Skill 不允许删除') return } try { await skillService.deleteSkill(skill.id) message.success('删除成功') fetchSkills() } catch (error) { message.error(`删除失败: ${(error as Error).message}`) } } const handleCreate = () => { // 打开新的向导 setWizardVisible(true) } // 从模板初始化 Skill(使用 skill-creator 的 init_skill.py 脚本) const handleInitFromTemplate = async () => { if (!templateSkillName.trim()) { message.warning('请输入 Skill 名称') return } // 验证格式:hyphen-case if (!/^[a-z0-9-]+$/.test(templateSkillName)) { message.warning('Skill 名称只能包含小写字母、数字和连字符 (hyphen-case)') return } setInitializing(true) setInitResult(null) try { const result = await skillService.initSkillTemplate( templateSkillName, templateOutputPath || undefined ) if (result.success) { setInitResult(result.output || 'Skill 模板初始化成功!') message.success('Skill 模板初始化成功!') // 自动填充表单 form.setFieldsValue({ id: templateSkillName, name: templateSkillName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '), category: 'general', behavior_guide: `# ${form.getFieldValue('name') || templateSkillName}\n\n## Overview\n\n[TODO: Describe what this skill does]\n\n## Behavior Guide\n\n[TODO: Add behavior guide content]`, tags: [] }) } } catch (error) { message.error(`初始化失败: ${(error as Error).message}`) } finally { setInitializing(false) } } // AI 生成 Skill const handleGenerate = async () => { if (!aiIdea.trim()) { message.warning('请输入您的想法或需求') return } setGenerating(true) try { const draft = await skillService.generateSkill(aiIdea, aiCategory) setGeneratedDraft(draft) // 将生成的数据填充到表单 form.setFieldsValue({ id: draft.suggested_id, name: draft.suggested_name, category: draft.suggested_category, behavior_guide: draft.behavior_guide, tags: draft.suggested_tags }) message.success('AI 生成成功!您可以继续编辑后保存') } catch (error) { message.error(`AI 生成失败: ${(error as Error).message}`) } finally { setGenerating(false) } } // AI 优化 Skill const handleOptimize = async () => { if (!generatedDraft && !editingSkill) { message.warning('请先生成或加载 Skill 内容') return } if (!optimizeRequirements.trim()) { message.warning('请输入优化需求') return } const currentContent = form.getFieldValue('behavior_guide') if (!currentContent) { message.warning('没有可优化的内容') return } setOptimizing(true) try { const skillId = form.getFieldValue('id') || 'temp' const result = await skillService.optimizeSkill(skillId, currentContent, optimizeRequirements) // 更新行为指导 form.setFieldsValue({ behavior_guide: result.behavior_guide }) // 更新 generatedDraft if (generatedDraft) { setGeneratedDraft({ ...generatedDraft, behavior_guide: result.behavior_guide }) } message.success('优化成功!') // 显示改进说明 if (result.improvements && result.improvements.length > 0) { Modal.info({ title: '优化改进', content: (
    {result.improvements.map((item, index) => (
  • {item}
  • ))}
), width: 600 }) } } catch (error) { message.error(`优化失败: ${(error as Error).message}`) } finally { setOptimizing(false) } } // 重新生成 const handleRegenerate = () => { setGeneratedDraft(null) handleGenerate() } const handleCreateSubmit = async () => { try { const values = await form.validateFields() setSubmitting(true) await skillService.createSkill({ id: values.id, name: values.name, content: values.behavior_guide, category: values.category }) message.success('创建成功') setCreateModalOpen(false) form.resetFields() fetchSkills() } catch (error) { message.error(`创建失败: ${(error as Error).message}`) } finally { setSubmitting(false) } } const handleEditSubmit = async () => { if (!editingSkill) return try { const values = await form.validateFields() setSubmitting(true) await skillService.updateSkill(editingSkill.id, { name: values.name, content: values.behavior_guide }) message.success('更新成功') setEditModalOpen(false) setEditingSkill(null) form.resetFields() fetchSkills() } catch (error) { message.error(`更新失败: ${(error as Error).message}`) } finally { setSubmitting(false) } } const columns: ColumnsType = [ { title: 'Skill ID', dataIndex: 'id', key: 'id', width: 200, render: (id: string, record: Skill) => ( {id} {record.type === 'builtin' && ( )} ) }, { title: '名称', dataIndex: 'name', key: 'name', render: (name: string, record: Skill) => ( {name} {record.type === 'builtin' && } ) }, { title: '类型', dataIndex: 'type', key: 'type', width: 100, filters: [ { text: '内置', value: 'builtin' }, { text: '用户', value: 'user' } ], onFilter: (value, record) => record.type === value, render: (type: string) => ( : undefined}> {type === 'builtin' ? '内置' : '用户'} ) }, { title: '分类', dataIndex: 'category', key: 'category', width: 120, filters: categories.map(cat => ({ text: cat, value: cat })), onFilter: (value, record) => record.category === value, render: (category: string) => {category} }, { title: '版本', dataIndex: 'version', key: 'version', width: 100, }, { title: '标签', dataIndex: 'tags', key: 'tags', width: 150, render: (tags: string[]) => ( {tags?.slice(0, 3).map(tag => ( {tag} ))} {tags?.length > 3 && +{tags.length - 3}} ) }, { title: '操作', key: 'actions', width: 280, fixed: 'right', render: (_, record) => ( {record.type === 'user' && ( <> handleDelete(record)} okText="确认" cancelText="取消" > )} {record.type === 'builtin' && ( <> )} ) } ] return (
Skills 管理中心 } extra={ } > {/* 技能生成任务状态 */} {skillGenerationTasks.length > 0 && ( 刷新 } > 当前有 {skillGenerationTasks.length} 个 Skill 正在生成中 )} {/* 搜索和筛选 */}
setSearchText(e.target.value)} prefix={} /> 分类: 视图: setViewMode(checked ? 'grid' : 'list')} checkedChildren="网格" unCheckedChildren="列表" /> 内置: {builtinSkills.length} 用户: {userSkills.length} 总计: {skills.length}
{/* Skills 列表 */} `共 ${total} 个 Skills` }} scroll={{ x: 1200 }} /> {/* Skill 详情抽屉 - 使用 Drawer 替代 Modal 避免重叠 */} {selectedSkill?.name} {selectedSkill?.type === 'builtin' && } } placement="right" width={720} open={detailDrawerOpen} onClose={() => setDetailDrawerOpen(false)} extra={ {selectedSkill?.type === 'user' && ( )} } > {selectedSkill && (
Skill ID:

{selectedSkill.id}

名称:

{selectedSkill.name}

版本: {selectedSkill.version}
类型: : undefined} style={{ marginLeft: '8px' }} > {selectedSkill.type === 'builtin' ? '内置 (只读)' : '用户 (可编辑)'}
分类: {selectedSkill.category}
{selectedSkill.created_at && (
创建时间:

{dayjs(selectedSkill.created_at).format('YYYY-MM-DD HH:mm:ss')}

)}
{selectedSkill.tags && selectedSkill.tags.length > 0 ? ( selectedSkill.tags.map(tag => {tag}) ) : ( )}
} onClick={() => { navigator.clipboard.writeText(selectedSkill.behavior_guide) message.success('已复制到剪贴板') }} > 复制 } >
                  {selectedSkill.behavior_guide}
                
                  {JSON.stringify(selectedSkill, null, 2)}
                
)}
{/* Skill 测试弹窗 */} 测试 Skill: {selectedSkill?.name} } open={testModalOpen} onCancel={() => { setTestModalOpen(false) setTestInput('') setTestResult('') }} width={800} footer={null} destroyOnHidden > {selectedSkill && (
测试输入: 输入要发送给 Skill 的内容