import { ref, computed } from 'vue' import { ElMessageBox, ElMessage } from 'element-plus' const API_BASE = 'http://localhost:8082' export interface Skill { id: string skill_name: string skill_type: string skill_desc: string path: string status: string created_at?: string updated_at?: string } export function useSkills() { // ============ 导入相关状态 ============ const fileInputRef = ref(null) const isImporting = ref(false) const isImportingDialog = ref(false) const importFile = ref(null) const importFileName = ref('') const importSkillName = ref('') const importSkillDesc = ref('') const importSkillContent = ref('') const isImportStep2 = ref(false) // 下拉菜单状态 const showDropdown = ref(false) const dropdownTimeout = ref(null) // ============ 技能列表状态 ============ const skills = ref([]) const skillsLoading = ref(false) // 搜索和筛选 const searchQuery = ref('') const filterStatus = ref('all') // 编辑状态 const isEditing = ref(false) const isEditingContent = ref(false) // 创建弹窗的第二步 const isEditingStep2 = ref(false) // 编辑弹窗的第二步 const isCreating = ref(false) const editingSkill = ref(null) // 表单 const editForm = ref({ skill_name: '', skill_desc: '', skill_type: 'user', }) const newSkillForm = ref({ skill_name: '', skill_desc: '', skill_type: 'user', }) const newSkillContent = ref('') const editSkillContent = ref('') // 编辑弹窗第二步的 SKILL.md 内容 // ============ 方法 ============ // 获取技能列表 const fetchSkills = async (type?: string) => { skillsLoading.value = true try { let url = `${API_BASE}/skill/list` if (type) { url += `?type=${type}` } const response = await fetch(url) const result = await response.json() if (result.list) { skills.value = result.list.map((skill: any) => ({ id: skill.id, skill_name: skill.skill_name, skill_type: skill.skill_type, skill_desc: skill.skill_desc || '', path: skill.path || '', status: skill.status || 'active', created_at: skill.created_at, updated_at: skill.updated_at, })) } return result.list || [] } catch (error) { console.error('Failed to fetch skills:', error) return [] } finally { skillsLoading.value = false } } // 更新技能 const updateSkill = async (id: string, skill: Partial) => { try { const response = await fetch(`${API_BASE}/skill/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(skill), }) const data = await response.json() await fetchSkills() return data } catch (error) { console.error('Failed to update skill:', error) throw error } } // 删除技能 const deleteSkill = async (id: string) => { try { const response = await fetch(`${API_BASE}/skill/${id}`, { method: 'DELETE', }) const data = await response.json() await fetchSkills() return data } catch (error) { console.error('Failed to delete skill:', error) throw error } } // 筛选 const filteredSkills = computed(() => { return skills.value.filter(skill => { const matchSearch = skill.skill_name.toLowerCase().includes(searchQuery.value.toLowerCase()) || (skill.skill_desc && skill.skill_desc.toLowerCase().includes(searchQuery.value.toLowerCase())) const matchStatus = filterStatus.value === 'all' || skill.status === filterStatus.value return matchSearch && matchStatus }) }) // 打开创建弹窗 const openCreate = () => { newSkillForm.value = { skill_name: '', skill_desc: '', skill_type: 'user' } newSkillContent.value = '' isCreating.value = true } // 关闭创建弹窗 const closeCreate = () => { isCreating.value = false newSkillContent.value = '' } // 第一步:点击 Next,跳转到编辑内容 const goToEditContent = () => { if (!newSkillForm.value.skill_name || !newSkillForm.value.skill_desc) { ElMessage.warning('Please fill in skill name and description') return } newSkillContent.value = `--- name: ${newSkillForm.value.skill_name} description: ${newSkillForm.value.skill_desc} --- # ${newSkillForm.value.skill_name} ${newSkillForm.value.skill_desc} ` isCreating.value = false isEditingContent.value = true } // 关闭编辑内容弹窗 const closeEditContent = () => { isEditingContent.value = false newSkillContent.value = '' } // 第二步:保存技能内容 const saveNewSkill = async () => { try { const blob = new Blob([newSkillContent.value], { type: 'text/markdown' }) const file = new File([blob], 'SKILL.md', { type: 'text/markdown' }) const formData = new FormData() formData.append('skill_name', newSkillForm.value.skill_name) formData.append('skill_desc', newSkillForm.value.skill_desc) formData.append('skill_type', newSkillForm.value.skill_type) formData.append('file', file) const response = await fetch(`${API_BASE}/skill/add`, { method: 'POST', body: formData, }) const data = await response.json() if (response.ok) { ElMessage.success('Skill created successfully') isEditingContent.value = false newSkillContent.value = '' await fetchSkills() } else { ElMessage.error(data.error || 'Failed to create skill') } } catch (error) { console.error('Failed to create skill:', error) ElMessage.error('Failed to create skill') } } // 打开编辑弹窗(第一步) const openEdit = async (skill: Skill) => { editingSkill.value = skill editForm.value = { skill_name: skill.skill_name, skill_desc: skill.skill_desc, skill_type: skill.skill_type, } // 读取现有的 SKILL.md 内容 console.log('Opening edit for skill:', skill) try { const response = await fetch(`${API_BASE}/skill/content?id=${skill.id}`) console.log('Content API response:', response.status, response.statusText) if (response.ok) { const text = await response.text() console.log('Skill content:', text.substring(0, 100)) editSkillContent.value = text } else { // API 返回 404 说明文件不存在,生成默认内容 console.log('File not found, using default content') editSkillContent.value = `--- name: ${skill.skill_name} description: ${skill.skill_desc || 'A custom skill'} category: ${skill.skill_type || 'user'} --- # ${skill.skill_name} ${skill.skill_desc || 'A custom skill'} ## Instructions Describe what this skill does and how it works. ## Tools List the tools this skill can use: - ## Examples Provide some examples of how to use this skill: \`\`\` Example 1: \`\`\` ` } } catch (error) { console.error('Error getting skill content:', error) // 网络错误也使用默认内容 editSkillContent.value = `--- name: ${skill.skill_name} description: ${skill.skill_desc || 'A custom skill'} category: ${skill.skill_type || 'user'} --- # ${skill.skill_name} ${skill.skill_desc || 'A custom skill'} ## Instructions Describe what this skill does and how it works. ## Tools List the tools this skill can use: - ## Examples Provide some examples of how to use this skill: \`\`\` Example 1: \`\`\` ` } isEditing.value = true } // 关闭编辑弹窗 const closeEdit = () => { isEditing.value = false isEditingStep2.value = false editingSkill.value = null editSkillContent.value = '' } // 编辑第一步:点击 Next,跳转到编辑内容 const goToEditStep2 = () => { if (!editForm.value.skill_name || !editForm.value.skill_desc) { ElMessage.warning('Please fill in skill name and description') return } isEditing.value = false isEditingStep2.value = true } // 编辑第二步:保存 const saveEditStep2 = async () => { if (!editingSkill.value) return try { // 将内容转换为文件上传 const blob = new Blob([editSkillContent.value], { type: 'text/markdown' }) const file = new File([blob], 'SKILL.md', { type: 'text/markdown' }) const formData = new FormData() formData.append('skill_name', editForm.value.skill_name) formData.append('skill_desc', editForm.value.skill_desc) formData.append('skill_type', editForm.value.skill_type) formData.append('file', file) const response = await fetch(`${API_BASE}/skill/${editingSkill.value.id}`, { method: 'PUT', body: formData, }) if (response.ok) { ElMessage.success('Skill updated successfully') await fetchSkills() isEditingStep2.value = false editingSkill.value = null editSkillContent.value = '' } else { const data = await response.json() ElMessage.error(data.error || 'Failed to update skill') } } catch (error) { console.error('Failed to update skill:', error) ElMessage.error('Failed to update skill') } } // 返回编辑第一步 const goBackToEditStep1 = () => { isEditingStep2.value = false isEditing.value = true } // 关闭编辑第二步弹窗(完全关闭) const closeEditStep2 = () => { isEditingStep2.value = false isEditing.value = false editingSkill.value = null editSkillContent.value = '' } // 切换状态 const toggleStatus = async (skill: Skill) => { const newStatus = skill.status === 'active' ? 'inactive' : 'active' try { await updateSkill(skill.id, { status: newStatus }) skill.status = newStatus } catch (error) { ElMessage.error('Failed to update status') } } // 删除技能(带确认) const handleDeleteSkill = async (id: string) => { try { await ElMessageBox.confirm('Are you sure you want to delete this skill?', 'Confirm Delete', { confirmButtonText: 'Delete', cancelButtonText: 'Cancel', type: 'warning', }) await deleteSkill(id) ElMessage.success('Skill deleted successfully') } catch (error: any) { if (error !== 'cancel') { console.error('Failed to delete skill:', error) ElMessage.error('Failed to delete skill') } } } // ============ 导入相关方法 ============ // 打开导入弹窗 const openImportDialog = () => { showDropdown.value = false isImportingDialog.value = true importFile.value = null importFileName.value = '' importSkillName.value = '' importSkillDesc.value = '' importSkillContent.value = '' isImportStep2.value = false } // 关闭导入弹窗 const closeImportDialog = () => { isImportingDialog.value = false importFile.value = null importFileName.value = '' isImportStep2.value = false } // 解析 SKILL.md 内容 const parseSkillContent = (content: string, fallbackName: string) => { let skillName = '' let skillDesc = '' const trimmedContent = content.trim() if (trimmedContent.startsWith('---')) { const endIndex = trimmedContent.indexOf('---', 3) if (endIndex > 3) { const yamlContent = trimmedContent.substring(3, endIndex).trim() const nameMatch = yamlContent.match(/name:\s*(.+)/) const descMatch = yamlContent.match(/description:\s*(.+)/) skillName = nameMatch ? nameMatch[1].trim() : fallbackName skillDesc = descMatch ? descMatch[1].trim() : '' const afterYaml = trimmedContent.substring(endIndex + 3).trim() if (afterYaml && !skillDesc) { skillDesc = afterYaml } } } if (!skillName) { const lines = trimmedContent.split('\n') skillName = lines[0]?.replace(/^#\s*/, '').trim() || fallbackName skillDesc = lines.slice(1).join('\n').trim() } return { skillName, skillDesc } } // 处理文件选择 const handleFileChange = async (event: Event) => { const input = event.target as HTMLInputElement if (!input.files || input.files.length === 0) return const file = input.files[0] importFile.value = file // 获取纯文件名,去除路径 const fileName = file.name.split(/[/\\]/).pop()?.replace('.md', '') || 'untitled' importFileName.value = fileName try { const content = await file.text() const { skillName, skillDesc } = parseSkillContent(content, fileName) importSkillName.value = skillName importSkillDesc.value = skillDesc importSkillContent.value = content isImportStep2.value = true } catch (error) { console.error('Failed to read file:', error) ElMessage.error('读取文件失败') } input.value = '' } // 提交导入 const submitImport = async () => { if (!importSkillName.value || !importSkillContent.value) { ElMessage.warning('请选择有效的技能文件') return } isImporting.value = true try { const blob = new Blob([importSkillContent.value], { type: 'text/markdown' }) const file = new File([blob], 'SKILL.md', { type: 'text/markdown' }) const formData = new FormData() formData.append('skill_name', importSkillName.value) formData.append('skill_desc', importSkillDesc.value) formData.append('file', file) const response = await fetch(`${API_BASE}/skill/add`, { method: 'POST', body: formData }) if (response.ok) { ElMessage.success(`Skill imported: ${importSkillName.value}`) await fetchSkills() closeImportDialog() } else { const err = await response.json() ElMessage.error(err.message || '导入失败') } } catch (error) { console.error('Import failed:', error) ElMessage.error('导入失败,请检查文件格式是否正确') } finally { isImporting.value = false } } // 处理文件选择(单个 SKILL.md 文件) const handleFolderSelect = async (event: Event) => { const input = event.target as HTMLInputElement const files = input.files if (!files || files.length === 0) return isImporting.value = true try { const file = files[0] // 读取文件内容 const content = await file.text() const fileName = file.name.replace('.md', '') const { skillName, skillDesc } = parseSkillContent(content, fileName) // 使用导入弹窗显示内容 importSkillName.value = skillName importSkillDesc.value = skillDesc importSkillContent.value = content isImportingDialog.value = true isImportStep2.value = true } catch (error) { console.error('Import failed:', error) ElMessage.error('导入失败,请检查文件格式是否正确') } finally { isImporting.value = false input.value = '' } } // 鼠标进入时显示 const onDropdownEnter = () => { if (dropdownTimeout.value) { clearTimeout(dropdownTimeout.value) dropdownTimeout.value = null } showDropdown.value = true } // 鼠标离开时延迟隐藏 const onDropdownLeave = () => { dropdownTimeout.value = window.setTimeout(() => { showDropdown.value = false }, 300) } // ============ 返回 ============ return { // State skills, skillsLoading, searchQuery, filterStatus, isEditing, isEditingStep2, isCreating, isEditingContent, editingSkill, editForm, newSkillForm, newSkillContent, editSkillContent, // Computed filteredSkills, // Methods fetchSkills, openCreate, closeCreate, goToEditContent, closeEditContent, saveNewSkill, openEdit, closeEdit, goToEditStep2, goBackToEditStep1, saveEditStep2, closeEditStep2, toggleStatus, deleteSkill: handleDeleteSkill, // 导入相关 fileInputRef, isImporting, isImportingDialog, importFileName, importSkillName, importSkillDesc, importSkillContent, isImportStep2, openImportDialog, closeImportDialog, handleFileChange, submitImport, handleFolderSelect, // 下拉菜单 showDropdown, dropdownTimeout, onDropdownEnter, onDropdownLeave, } }