-
+
-
-
@@ -222,6 +152,14 @@ const showDropdown = ref(false)
{{ skill.created_at ? new Date(skill.created_at).toLocaleDateString() : '-' }} |
+
@@ -324,11 +262,156 @@ const showDropdown = ref(false)
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit Skill Content
+ Configure skill details in SKILL.md format
+
+
+
+
+
+
+
+
+
+
+
+
+ Tip: SKILL.md should contain YAML front matter with name and description, followed by the skill implementation in markdown.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import Skill
+ Select a file or folder to import
+
+
+
+
+
+
+
+
+
+ Import File
+ Select a single SKILL.md file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit Skill Content
+ Review and edit imported skill content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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,
+ }
+}
|