Files
X-Agents/web/src/views/skill/skill.ts
DESKTOP-72TV0V4\caoxiaozhu 19f5c79d58 feat: 更新前端页面和agent应用
- Agents.vue: 更新agent列表和创建功能
- Skill.vue: 更新skill页面
- skill.ts: 更新skill编辑逻辑
- agent/app/main.py: 更新agent应用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:17:08 +08:00

617 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HTMLInputElement | null>(null)
const isImporting = ref(false)
const isImportingDialog = ref(false)
const importFile = ref<File | null>(null)
const importFileName = ref('')
const importSkillName = ref('')
const importSkillDesc = ref('')
const importSkillContent = ref('')
const isImportStep2 = ref(false)
// 下拉菜单状态
const showDropdown = ref(false)
const dropdownTimeout = ref<number | null>(null)
// ============ 技能列表状态 ============
const skills = ref<Skill[]>([])
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<Skill | null>(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<Skill>) => {
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,
}
}