- 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>
224 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|