feat: 新增技能功能
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user