feat: 新增技能功能

This commit is contained in:
duanshuwen
2026-04-11 10:55:11 +08:00
parent 825fe36967
commit b1ca06bb07
5 changed files with 84 additions and 92 deletions

View File

@@ -1,7 +1,7 @@
<template>
<layout>
<div class="bg-[#f9f7f0] box-border w-full h-full flex rounded-[16px] overflow-hidden">
<div class="w-full max-w-5xl mx-auto flex flex-col h-full p-10 pt-12">
<div class="bg-white box-border w-full h-full flex rounded-[16px] overflow-hidden">
<div class="w-full flex flex-col h-full p-10 pt-12">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-start justify-between mb-6 shrink-0 gap-4">
<div>
@@ -165,9 +165,9 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSkillsStore, FETCH_ERROR_CODES, INSTALL_ERROR_CODES } from '@src/store/skills'
import { apiGetSkillsDir, apiOpenSkillsDir } from '@src/lib/skills-api'
import type { Skill } from '@src/lib/skills-types'
import { useSkillsStore, FETCH_ERROR_CODES } from '@src/store/skills'
import { MOCK_SKILLS, MOCK_MARKETPLACE } from '@src/lib/skills-api'
import type { Skill, MarketplaceSkill } from '@src/lib/skills-types'
import { ElMessage } from 'element-plus'
import {
RiSearchLine,
@@ -191,9 +191,12 @@ const selectedSkill = ref<Skill | null>(null)
const selectedSource = ref<'all' | 'built-in' | 'marketplace'>('all')
const skillsDirPath = ref('~/.zn-ai/skills')
onMounted(async () => {
store.fetchSkills()
skillsDirPath.value = await apiGetSkillsDir()
onMounted(() => {
// 演示模式:直接填充本地 Mock 数据,避免调用后端接口
store.skills = MOCK_SKILLS as Skill[]
store.loading = false
// store.fetchSkills()
// skillsDirPath.value = await apiGetSkillsDir()
})
const safeSkills = computed(() => (Array.isArray(store.skills) ? store.skills : []))
@@ -244,11 +247,8 @@ async function bulkToggleVisible(enable: boolean) {
let succeeded = 0
for (const skill of candidates) {
try {
if (enable) {
await store.enableSkill(skill.id)
} else {
await store.disableSkill(skill.id)
}
// Demo mode: skip store.enableSkill / store.disableSkill API calls
skill.enabled = enable
succeeded += 1
} catch {
// Continue to next skill
@@ -263,31 +263,15 @@ async function bulkToggleVisible(enable: boolean) {
}
async function handleToggle(skillId: string, enable: boolean) {
try {
if (enable) {
await store.enableSkill(skillId)
ElMessage.success(t('skills.toast.enabled'))
} else {
await store.disableSkill(skillId)
ElMessage.success(t('skills.toast.disabled'))
}
} catch (err) {
ElMessage.error(String(err))
}
const skill = safeSkills.value.find((s: Skill) => s.id === skillId)
if (!skill || skill.isCore) return
skill.enabled = enable
ElMessage.success(enable ? t('skills.toast.enabled') : t('skills.toast.disabled'))
}
async function handleOpenSkillsFolder() {
try {
await apiOpenSkillsDir()
ElMessage.success(t('skills.toast.copiedPath'))
} catch (err) {
const msg = String(err)
if (msg.toLowerCase().includes('copied')) {
ElMessage.info(msg)
} else {
ElMessage.error(t('skills.toast.failedOpenFolder') + ': ' + msg)
}
}
// Demo mode: skip API call
ElMessage.info(t('skills.toast.copiedPath') + ' (Demo)')
}
function openMarketplace() {
@@ -297,46 +281,54 @@ function openMarketplace() {
watch(installDrawerOpen, (open) => {
if (open) {
store.searchSkills('')
store.searchResults = MOCK_MARKETPLACE as any
// store.searchSkills('')
}
})
watch(installQuery, (q) => {
if (!installDrawerOpen.value) return
const query = q.trim()
const all = MOCK_MARKETPLACE
if (query.length === 0) {
store.searchSkills('')
store.searchResults = all as any
return
}
// debounce 300ms handled by watch pattern isn't ideal without clearTimeout,
// but for simplicity we call directly. In a real app use useDebounceFn from vueuse.
setTimeout(() => {
store.searchSkills(query)
const lower = query.toLowerCase()
store.searchResults = all.filter(
(s: MarketplaceSkill) => s.name.toLowerCase().includes(lower) || s.description.toLowerCase().includes(lower)
) as any
// store.searchSkills(query)
}, 300)
})
async function handleInstall(slug: string) {
try {
await store.installSkill(slug)
await store.enableSkill(slug)
ElMessage.success(t('skills.toast.installed'))
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
if (INSTALL_ERROR_CODES.has(errorMessage)) {
ElMessage({ type: 'error', message: String(t(`skills.toast.${errorMessage}`, { path: skillsDirPath.value })), duration: 10000 })
} else {
ElMessage.error(t('skills.toast.failedInstall') + ': ' + errorMessage)
}
// Demo mode: 模拟安装
const ms = MOCK_MARKETPLACE.find((s: MarketplaceSkill) => s.slug === slug)
if (ms && !store.skills.some((s: Skill) => s.id === slug || s.name === ms.name)) {
store.skills.push({
id: slug,
slug,
name: ms.name,
description: ms.description,
enabled: true,
icon: '📦',
version: ms.version || '1.0.0',
isCore: false,
isBundled: false,
source: 'openclaw-managed',
author: ms.author,
} as Skill)
}
ElMessage.success(t('skills.toast.installed') + ' (Demo)')
}
async function handleUninstall(slug: string) {
try {
await store.uninstallSkill(slug)
ElMessage.success(t('skills.toast.uninstalled'))
} catch (err) {
ElMessage.error(t('skills.toast.failedUninstall') + ': ' + String(err))
}
// Demo mode: 模拟卸载
const idx = store.skills.findIndex((s: Skill) => s.slug === slug)
if (idx > -1) store.skills.splice(idx, 1)
ElMessage.success(t('skills.toast.uninstalled') + ' (Demo)')
}
</script>