From e2c9bbd0d1dd7b7d90a74d33bcbc044e6f6729b3 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Thu, 12 Mar 2026 15:23:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0skill=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=89=8D=E7=AB=AF=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 web/src/views/skill/skill.ts skill视图组件 - 更新 Agents.vue 页面 - 更新 Skill.vue 页面 - 移除旧的 useSkills.ts Co-Authored-By: Claude Opus 4.6 --- web/src/views/Agents.vue | 739 ++++++++++++++++++++++++++++++++++- web/src/views/Skill.vue | 285 +++++++++----- web/src/views/skill/skill.ts | 496 +++++++++++++++++++++++ 3 files changed, 1401 insertions(+), 119 deletions(-) create mode 100644 web/src/views/skill/skill.ts diff --git a/web/src/views/Agents.vue b/web/src/views/Agents.vue index 399a93a..caae301 100644 --- a/web/src/views/Agents.vue +++ b/web/src/views/Agents.vue @@ -1,5 +1,17 @@ + + + + + + diff --git a/web/src/views/skill/skill.ts b/web/src/views/skill/skill.ts new file mode 100644 index 0000000..3c40c6b --- /dev/null +++ b/web/src/views/skill/skill.ts @@ -0,0 +1,496 @@ +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 isCreating = ref(false) + const isEditingContent = 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 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 = (skill: Skill) => { + editingSkill.value = skill + editForm.value = { + skill_name: skill.skill_name, + skill_desc: skill.skill_desc, + skill_type: skill.skill_type, + } + isEditing.value = true + } + + // 关闭编辑弹窗 + const closeEdit = () => { + isEditing.value = false + editingSkill.value = null + } + + // 保存编辑 + const saveEdit = async () => { + try { + await updateSkill(editingSkill.value!.id, { + skill_name: editForm.value.skill_name, + skill_desc: editForm.value.skill_desc, + skill_type: editForm.value.skill_type, + }) + ElMessage.success('Skill updated successfully') + isEditing.value = false + } catch (error) { + ElMessage.error('Failed to update skill') + } + } + + // 切换状态 + 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 + importFileName.value = file.name + + try { + const content = await file.text() + const { skillName, skillDesc } = parseSkillContent(content, file.name.replace('.md', '')) + + 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 + } + } + + // 处理文件夹选择 + 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 folder = files[0].webkitRelativePath?.split('/')[0] || files[0].name + + // 查找 SKILL.md 文件 + let skillMdFile: File | null = null + for (let i = 0; i < files.length; i++) { + const file = files[i] + if (file.name === 'SKILL.md' || file.webkitRelativePath?.endsWith('/SKILL.md')) { + skillMdFile = file + break + } + } + + if (!skillMdFile) { + ElMessage.error('导入失败:所选文件夹中未找到 SKILL.md 文件') + return + } + + const content = await skillMdFile.text() + const { skillName, skillDesc } = parseSkillContent(content, folder) + + // 使用导入弹窗显示内容 + 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, + isCreating, + isEditingContent, + editingSkill, + editForm, + newSkillForm, + newSkillContent, + // Computed + filteredSkills, + // Methods + fetchSkills, + openCreate, + closeCreate, + goToEditContent, + closeEditContent, + saveNewSkill, + openEdit, + closeEdit, + saveEdit, + toggleStatus, + deleteSkill: handleDeleteSkill, + // 导入相关 + fileInputRef, + isImporting, + isImportingDialog, + importFileName, + importSkillName, + importSkillDesc, + importSkillContent, + isImportStep2, + openImportDialog, + closeImportDialog, + handleFileChange, + submitImport, + handleFolderSelect, + // 下拉菜单 + showDropdown, + dropdownTimeout, + onDropdownEnter, + onDropdownLeave, + } +}