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

@@ -9,9 +9,9 @@
"marketplace": "Marketplace"
},
"filter": {
"all": "All ({{count}})",
"builtIn": "Built-in ({{count}})",
"marketplace": "Marketplace ({{count}})"
"all": "All ({count})",
"builtIn": "Built-in ({count})",
"marketplace": "Marketplace ({count})"
},
"search": "Search skills...",
"searchMarketplace": "Search marketplace...",
@@ -83,17 +83,17 @@
"copiedPath": "Path copied",
"failedCopyPath": "Failed to copy path",
"failedOpenActualFolder": "Failed to open actual skill folder",
"searchTimeoutError": "Search timed out, check network. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{{path}}\"",
"installTimeoutError": "Installation timed out, check network. You can also download the ZIP from ClawHub.ai and extract it to \"{{path}}\"",
"searchRateLimitError": "Search rate limit exceeded. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{{path}}\"",
"installRateLimitError": "Installation rate limit exceeded. You can also download the ZIP from ClawHub.ai and extract it to \"{{path}}\"",
"searchTimeoutError": "Search timed out, check network. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{path}\"",
"installTimeoutError": "Installation timed out, check network. You can also download the ZIP from ClawHub.ai and extract it to \"{path}\"",
"searchRateLimitError": "Search rate limit exceeded. You can also search on ClawHub.ai, download the ZIP, and extract it to \"{path}\"",
"installRateLimitError": "Installation rate limit exceeded. You can also download the ZIP from ClawHub.ai and extract it to \"{path}\"",
"fetchTimeoutError": "Fetching skills timed out, please check your network connection.",
"fetchRateLimitError": "Fetching skills rate limit exceeded, please try again later.",
"noBatchEnableTargets": "All visible skills are already enabled.",
"noBatchDisableTargets": "All visible skills are already disabled.",
"batchEnabled": "{{count}} skills enabled.",
"batchDisabled": "{{count}} skills disabled.",
"batchPartial": "Updated {{success}} / {{total}} skills. Some items failed."
"batchEnabled": "{count} skills enabled.",
"batchDisabled": "{count} skills disabled.",
"batchPartial": "Updated {success} / {total} skills. Some items failed."
},
"marketplace": {
"title": "Marketplace",
@@ -102,7 +102,7 @@
"sourceLabel": "Source",
"sourceClawHub": "ClawHub",
"securityNote": "Click skill card to view its documentation and security information on ClawHub before installation.",
"manualInstallHint": "Network issues? You can always download skill ZIP archives from ClawHub.ai and extract them manually into \"{{path}}\".",
"manualInstallHint": "Network issues? You can always download skill ZIP archives from ClawHub.ai and extract them manually into \"{path}\".",
"searching": "Searching ClawHub...",
"noResults": "No skills found matching your search.",
"emptyPrompt": "Search for new skills to expand your capabilities.",

View File

@@ -9,9 +9,9 @@
"marketplace": "マーケットプレイス"
},
"filter": {
"all": "すべて ({{count}})",
"builtIn": "内蔵 ({{count}})",
"marketplace": "マーケットプレイス ({{count}})"
"all": "すべて ({count})",
"builtIn": "内蔵 ({count})",
"marketplace": "マーケットプレイス ({count})"
},
"search": "スキルを検索...",
"searchMarketplace": "マーケットプレイスを検索...",
@@ -83,17 +83,17 @@
"copiedPath": "パスをコピーしました",
"failedCopyPath": "パスのコピーに失敗しました",
"failedOpenActualFolder": "スキルの実際のフォルダを開けませんでした",
"searchTimeoutError": "検索がタイムアウトしました。ClawHub.aiで検索してZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
"installTimeoutError": "インストールがタイムアウトしました。ClawHub.aiでZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
"searchRateLimitError": "検索リクエストの制限を超過しました。ClawHub.aiで検索してZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
"installRateLimitError": "インストールリクエストの制限を超過しました。ClawHub.aiからZIPをダウンロードし、\"{{path}}\" に展開することも可能です",
"searchTimeoutError": "検索がタイムアウトしました。ClawHub.aiで検索してZIPをダウンロードし、\"{path}\" に展開することも可能です",
"installTimeoutError": "インストールがタイムアウトしました。ClawHub.aiでZIPをダウンロードし、\"{path}\" に展開することも可能です",
"searchRateLimitError": "検索リクエストの制限を超過しました。ClawHub.aiで検索してZIPをダウンロードし、\"{path}\" に展開することも可能です",
"installRateLimitError": "インストールリクエストの制限を超過しました。ClawHub.aiからZIPをダウンロードし、\"{path}\" に展開することも可能です",
"fetchTimeoutError": "スキルリストの取得がタイムアウトしました。ネットワークを確認してください。",
"fetchRateLimitError": "スキルリスト取得のリクエスト制限を超過しました。後でお試しください。",
"noBatchEnableTargets": "表示中のスキルはすべて有効です。",
"noBatchDisableTargets": "表示中のスキルはすべて無効です。",
"batchEnabled": "{{count}} 件のスキルを有効化しました。",
"batchDisabled": "{{count}} 件のスキルを無効化しました。",
"batchPartial": "{{success}} / {{total}} 件を更新しました。一部失敗しています。"
"batchEnabled": "{count} 件のスキルを有効化しました。",
"batchDisabled": "{count} 件のスキルを無効化しました。",
"batchPartial": "{success} / {total} 件を更新しました。一部失敗しています。"
},
"marketplace": {
"title": "マーケットプレイス",
@@ -102,7 +102,7 @@
"sourceLabel": "ソース",
"sourceClawHub": "ClawHub",
"securityNote": "インストール前にスキルカードをクリックして、ClawHubでドキュメントとセキュリティ情報を確認してください。",
"manualInstallHint": "ネットワークに問題がありますかいつでもClawHub.aiからスキルのZIPをダウンロードし、手動で \"{{path}}\" に展開してインストールできます。",
"manualInstallHint": "ネットワークに問題がありますかいつでもClawHub.aiからスキルのZIPをダウンロードし、手動で \"{path}\" に展開してインストールできます。",
"searching": "ClawHubを検索中...",
"noResults": "検索に一致するスキルが見つかりません。",
"emptyPrompt": "新しいスキルを検索して機能を拡張しましょう。",

View File

@@ -9,9 +9,9 @@
"marketplace": "市场"
},
"filter": {
"all": "全部 ({{count}})",
"builtIn": "内置 ({{count}})",
"marketplace": "市场 ({{count}})"
"all": "全部 ({count})",
"builtIn": "内置 ({count})",
"marketplace": "市场 ({count})"
},
"search": "搜索技能...",
"searchMarketplace": "搜索市场...",
@@ -83,17 +83,17 @@
"copiedPath": "路径已复制",
"failedCopyPath": "复制路径失败",
"failedOpenActualFolder": "打开技能实际目录失败",
"searchTimeoutError": "搜索超时,请检查网络。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{{path}}\"",
"installTimeoutError": "安装超时,请检查网络。您也可在 ClawHub.ai 下载该技能压缩包,解压到 \"{{path}}\"",
"searchRateLimitError": "搜索请求过于频繁。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{{path}}\"",
"installRateLimitError": "安装请求过于频繁。您也可在 ClawHub.ai 下载该技能压缩包,解压到 \"{{path}}\"",
"searchTimeoutError": "搜索超时,请检查网络。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{path}\"",
"installTimeoutError": "安装超时,请检查网络。您也可在 ClawHub.ai 下载该技能压缩包,解压到 \"{path}\"",
"searchRateLimitError": "搜索请求过于频繁。您也可访问 ClawHub.ai 搜索并下载压缩包,解压到 \"{path}\"",
"installRateLimitError": "安装请求过于频繁。您也可在 ClawHub.ai 下载该技能压缩包,解压到 \"{path}\"",
"fetchTimeoutError": "获取技能列表超时,请检查网络。",
"fetchRateLimitError": "获取技能列表请求过于频繁,请稍后再试。",
"noBatchEnableTargets": "当前可见技能都已启用。",
"noBatchDisableTargets": "当前可见技能都已禁用。",
"batchEnabled": "已启用 {{count}} 个技能。",
"batchDisabled": "已禁用 {{count}} 个技能。",
"batchPartial": "已更新 {{success}} / {{total}} 个技能,部分操作失败。"
"batchEnabled": "已启用 {count} 个技能。",
"batchDisabled": "已禁用 {count} 个技能。",
"batchPartial": "已更新 {success} / {total} 个技能,部分操作失败。"
},
"marketplace": {
"title": "市场",
@@ -102,7 +102,7 @@
"sourceLabel": "来源",
"sourceClawHub": "ClawHub",
"securityNote": "安装前请点击技能卡片,在 ClawHub 上查看其文档和安全信息。",
"manualInstallHint": "遇到网络问题?您可以随时从 ClawHub.ai 下载技能压缩包,并将其解压至 \"{{path}}\" 目录来完成手动安装。",
"manualInstallHint": "遇到网络问题?您可以随时从 ClawHub.ai 下载技能压缩包,并将其解压至 \"{path}\" 目录来完成手动安装。",
"searching": "正在搜索 ClawHub...",
"noResults": "未找到匹配的技能。",
"emptyPrompt": "搜索新技能以扩展您的能力。",

View File

@@ -2,7 +2,7 @@ import { hostApiFetch } from './host-api';
import type { Skill, MarketplaceSkill } from './skills-types';
// Mock data for UI development when backend is not ready
const MOCK_SKILLS: Skill[] = [
export const MOCK_SKILLS: Skill[] = [
{ id: '1password', slug: '1password', name: '1password', description: 'Set up and use 1Password CLI (op). Use when installing the CLI, enabling desktop app integration, signing in...', enabled: true, icon: '🔐', version: '1.0.0', isCore: false, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/1password' },
{ id: 'apple-notes', slug: 'apple-notes', name: 'apple-notes', description: 'Manage Apple Notes via the `memo` CLI on macOS (create, view, edit, delete, search, move, and export notes...', enabled: true, icon: '📝', version: '1.0.0', isCore: false, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/apple-notes' },
{ id: 'apple-reminders', slug: 'apple-reminders', name: 'apple-reminders', description: 'Manage Apple Reminders via remindctl CLI (list, add, edit, complete, delete). Supports lists, date filters, and...', enabled: true, icon: '⏰', version: '1.0.0', isCore: false, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/apple-reminders' },
@@ -15,7 +15,7 @@ const MOCK_SKILLS: Skill[] = [
{ id: 'memory', slug: 'memory', name: 'memory', description: 'Store and retrieve long-term memories.', enabled: true, icon: '🧠', version: '1.0.0', isCore: true, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/memory' },
];
const MOCK_MARKETPLACE: MarketplaceSkill[] = [
export const MOCK_MARKETPLACE: MarketplaceSkill[] = [
{ slug: 'notion', name: 'notion', description: 'Read and write pages in Notion workspaces.', version: '1.0.0', author: 'clawhub' },
{ slug: 'linear', name: 'linear', description: 'Manage Linear issues and projects.', version: '1.0.0', author: 'clawhub' },
{ slug: 'slack', name: 'slack', description: 'Send messages and search channels in Slack.', version: '1.0.0', author: 'clawhub' },

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>