feat: 新增技能功能
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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": "新しいスキルを検索して機能を拡張しましょう。",
|
||||
|
||||
@@ -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": "搜索新技能以扩展您的能力。",
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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