Files
zclaw_openfang/desktop/src/components/Settings/Skills.tsx
iven 48a430fc97 refactor(skills): add skill-adapter and refactor SkillMarket
- Add skill-adapter.ts to bridge configStore and UI skill formats
- Refactor SkillMarket to use new skill-adapter instead of skill-discovery
- Add health check state to connectionStore
- Update multiple components with improved typing
- Clean up test artifacts and add new test results
- Update README and add skill-market-mvp plan

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

224 lines
8.1 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { silentErrorHandler } from '../../lib/error-utils';
import { Wrench, Zap, FileCode, Globe, Mail, Database, Search, MessageSquare } from 'lucide-react';
// ZCLAW 内置系统技能
const SYSTEM_SKILLS = [
{
id: 'code-assistant',
name: '代码助手',
description: '代码编写、调试、重构和优化',
category: '开发',
icon: FileCode,
},
{
id: 'web-search',
name: '网络搜索',
description: '实时搜索互联网信息',
category: '信息',
icon: Search,
},
{
id: 'file-manager',
name: '文件管理',
description: '文件读写、搜索和整理',
category: '系统',
icon: Database,
},
{
id: 'web-browsing',
name: '网页浏览',
description: '访问和解析网页内容',
category: '信息',
icon: Globe,
},
{
id: 'email-handler',
name: '邮件处理',
description: '发送和管理电子邮件',
category: '通讯',
icon: Mail,
},
{
id: 'chat-skill',
name: '对话技能',
description: '自然语言对话和问答',
category: '交互',
icon: MessageSquare,
},
{
id: 'automation',
name: '自动化任务',
description: '自动化工作流程执行',
category: '系统',
icon: Zap,
},
{
id: 'tool-executor',
name: '工具执行器',
description: '执行系统命令和脚本',
category: '系统',
icon: Wrench,
},
];
export function Skills() {
const connectionState = useConnectionStore((s) => s.connectionState);
const quickConfig = useConfigStore((s) => s.quickConfig);
const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const connected = connectionState === 'connected';
const [extraDir, setExtraDir] = useState('');
const [activeFilter, setActiveFilter] = useState<'all' | 'system' | 'builtin' | 'extra'>('all');
useEffect(() => {
if (connected) {
loadSkillsCatalog().catch(silentErrorHandler('Skills'));
}
}, [connected]);
const extraDirs = quickConfig.skillsExtraDirs || [];
const handleAddDir = async () => {
const nextDir = extraDir.trim();
if (!nextDir) return;
const nextDirs = Array.from(new Set([...extraDirs, nextDir]));
await saveQuickConfig({ skillsExtraDirs: nextDirs });
setExtraDir('');
await loadSkillsCatalog();
};
const filteredCatalog = skillsCatalog.filter(skill => {
if (activeFilter === 'all') return true;
if (activeFilter === 'builtin') return skill.source === 'builtin';
if (activeFilter === 'extra') return skill.source === 'extra';
return true;
});
return (
<div className="max-w-3xl">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-bold text-gray-900"></h1>
<button
onClick={() => { loadSkillsCatalog().catch(silentErrorHandler('Skills')); }}
className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors"
>
</button>
</div>
{!connected && (
<div className="bg-gray-50/50 border border-gray-200 rounded-xl p-4 mb-6 text-center text-sm text-gray-500 shadow-sm">
Gateway Gateway
</div>
)}
{/* 系统技能 */}
<div className="mb-6">
<h3 className="text-sm font-semibold text-gray-700 mb-3">ZCLAW </h3>
<div className="grid grid-cols-2 gap-3">
{SYSTEM_SKILLS.map((skill) => {
const Icon = skill.icon;
return (
<div
key={skill.id}
className="bg-white rounded-xl border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center flex-shrink-0">
<Icon className="w-5 h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">{skill.name}</span>
<span className="text-[10px] px-1.5 py-0.5 bg-purple-50 text-purple-600 rounded">
{skill.category}
</span>
</div>
<p className="text-xs text-gray-500 mt-1">{skill.description}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6 mb-6 shadow-sm">
<h3 className="font-medium mb-2 text-gray-900"></h3>
<p className="text-xs text-gray-500 mb-4"> SKILL.md Gateway skills.load.extraDirs </p>
<div className="flex gap-2">
<input
type="text"
value={extraDir}
onChange={(e) => setExtraDir(e.target.value)}
placeholder="输入额外技能目录"
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 focus:outline-none"
/>
<button
onClick={() => { handleAddDir().catch(silentErrorHandler('Skills')); }}
className="text-xs text-gray-500 px-4 py-2 border border-gray-200 rounded-lg hover:text-gray-700 transition-colors"
>
</button>
</div>
{extraDirs.length > 0 && (
<div className="mt-4 space-y-2">
{extraDirs.map((dir) => (
<div key={dir} className="text-xs text-gray-500 bg-gray-50 border border-gray-100 rounded-lg px-3 py-2">
{dir}
</div>
))}
</div>
)}
</div>
{/* Gateway 技能 */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-700">Gateway </h3>
<div className="flex items-center gap-2">
{['all', 'builtin', 'extra'].map((filter) => (
<button
key={filter}
onClick={() => setActiveFilter(filter as typeof activeFilter)}
className={`text-xs px-2 py-1 rounded-md transition-colors ${
activeFilter === filter
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:bg-gray-100'
}`}
>
{filter === 'all' ? '全部' : filter === 'builtin' ? '内置' : '额外'}
</button>
))}
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 shadow-sm divide-y divide-gray-100">
{filteredCatalog.length > 0 ? filteredCatalog.map((skill) => (
<div key={skill.id} className="p-4">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-sm font-medium text-gray-900">{skill.name}</div>
<div className="text-xs text-gray-500 mt-1 break-all">{skill.path}</div>
</div>
<span className={`text-xs px-2 py-1 rounded-full ${skill.source === 'builtin' ? 'bg-blue-50 text-blue-600' : 'bg-gray-100 text-gray-500'}`}>
{skill.source === 'builtin' ? '内置' : '额外'}
</span>
</div>
</div>
)) : (
<div className="bg-gray-50 rounded-xl p-8 text-center">
<p className="text-sm text-gray-400"></p>
<p className="text-xs text-gray-300 mt-1"> Gateway </p>
</div>
)}
</div>
</div>
);
}