feat: 新增技能相关功能

This commit is contained in:
duanshuwen
2026-04-10 23:03:56 +08:00
parent 90a3ff6f77
commit 825fe36967
17 changed files with 2295 additions and 27 deletions

174
src/lib/skills-api.ts Normal file
View File

@@ -0,0 +1,174 @@
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[] = [
{ 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' },
{ id: 'bear-notes', slug: 'bear-notes', name: 'bear-notes', description: 'Create, search, and manage Bear notes via grizzly CLI.', enabled: true, icon: '🐻', version: '1.0.0', isCore: false, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/bear-notes' },
{ id: 'fetch-hacker-news', slug: 'fetch-hacker-news', name: 'fetch-hacker-news', description: 'Fetch top stories from Hacker News.', enabled: false, icon: '📰', version: '1.0.0', isCore: false, isBundled: false, source: 'openclaw-managed', baseDir: '~/.openclaw/skills/fetch-hacker-news' },
{ id: 'github', slug: 'github', name: 'github', description: 'Interact with GitHub issues, pull requests, repositories, and more.', enabled: true, icon: '🐙', version: '1.2.0', isCore: false, isBundled: false, source: 'openclaw-managed', baseDir: '~/.openclaw/skills/github' },
{ id: 'todoist', slug: 'todoist', name: 'todoist', description: 'Manage Todoist tasks and projects.', enabled: true, icon: '✅', version: '1.0.0', isCore: false, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/todoist' },
{ id: 'zotero', slug: 'zotero', name: 'zotero', description: 'Search and manage Zotero references.', enabled: false, icon: '📚', version: '1.0.0', isCore: false, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/zotero' },
{ id: 'browser-use', slug: 'browser-use', name: 'browser-use', description: 'Use a browser to search and navigate the web.', enabled: true, icon: '🌐', version: '2.0.0', isCore: true, isBundled: true, source: 'openclaw-bundled', baseDir: '/Applications/ClawX.app/Contents/Resources/openclaw/skills/browser-use' },
{ 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[] = [
{ 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' },
{ slug: 'discord', name: 'discord', description: 'Send messages and manage Discord servers.', version: '1.0.0', author: 'clawhub' },
{ slug: 'figma', name: 'figma', description: 'Search and manage Figma files and comments.', version: '1.0.0', author: 'clawhub' },
{ slug: 'spotify', name: 'spotify', description: 'Control Spotify playback and manage playlists.', version: '1.0.0', author: 'clawhub' },
];
export async function apiFetchSkills(): Promise<Skill[]> {
try {
const configs = await hostApiFetch<Record<string, any>>('/api/skills/configs');
const listResult = await hostApiFetch<{ success: boolean; results?: any[]; error?: string }>('/api/clawhub/list');
let skills: Skill[] = [];
if (configs && typeof configs === 'object' && Object.keys(configs).length > 0) {
// Support extended config format that includes metadata (for zn-ai without gateway)
skills = Object.entries(configs).map(([key, cfg]: [string, any]) => ({
id: key,
slug: cfg.slug || key,
name: cfg.name || key,
description: cfg.description || '',
enabled: cfg.enabled !== false,
icon: cfg.icon || '📦',
version: cfg.version || '1.0.0',
author: cfg.author,
config: cfg.config || { apiKey: cfg.apiKey, env: cfg.env },
isCore: cfg.isCore || false,
isBundled: cfg.isBundled ?? true,
source: cfg.source,
baseDir: cfg.baseDir,
filePath: cfg.filePath,
}));
} else if (listResult?.success && listResult.results && listResult.results.length > 0) {
skills = listResult.results.map((item: any) => ({
id: item.slug,
slug: item.slug,
name: item.slug,
description: 'Recently installed, initializing...',
enabled: false,
icon: '⌛',
version: item.version || 'unknown',
source: item.source || 'openclaw-managed',
baseDir: item.baseDir,
isCore: false,
isBundled: false,
}));
}
if (skills.length === 0) {
return MOCK_SKILLS;
}
return skills;
} catch (error) {
console.error('Failed to fetch skills:', error);
return MOCK_SKILLS;
}
}
export async function apiSearchSkills(query: string): Promise<MarketplaceSkill[]> {
try {
const result = await hostApiFetch<{ success: boolean; results?: MarketplaceSkill[]; error?: string }>('/api/clawhub/search', {
method: 'POST',
body: JSON.stringify({ query }),
});
if (result?.success && result.results && result.results.length > 0) {
return result.results;
}
if (!query) return MOCK_MARKETPLACE;
const q = query.toLowerCase();
return MOCK_MARKETPLACE.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
} catch (error) {
console.error('Search error:', error);
if (!query) return MOCK_MARKETPLACE;
const q = query.toLowerCase();
return MOCK_MARKETPLACE.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
}
}
export async function apiInstallSkill(slug: string, version?: string): Promise<void> {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/install', {
method: 'POST',
body: JSON.stringify({ slug, version }),
});
if (!result?.success) {
throw new Error(result?.error || 'Install failed');
}
}
export async function apiUninstallSkill(slug: string): Promise<void> {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/uninstall', {
method: 'POST',
body: JSON.stringify({ slug }),
});
if (!result?.success) {
throw new Error(result?.error || 'Uninstall failed');
}
}
export async function apiUpdateSkillConfig(
skillKey: string,
config: { apiKey?: string; env?: Record<string, string>; enabled?: boolean }
): Promise<void> {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/skills/config', {
method: 'PUT',
body: JSON.stringify({ skillKey, ...config }),
});
if (!result?.success) {
throw new Error(result?.error || 'Save failed');
}
}
export async function apiOpenSkillPath(skillKey: string, slug?: string, baseDir?: string): Promise<void> {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-path', {
method: 'POST',
body: JSON.stringify({ skillKey, slug, baseDir }),
});
if (!result?.success) {
throw new Error(result?.error || 'Open path failed');
}
}
export async function apiOpenSkillReadme(skillKey: string, slug?: string, baseDir?: string): Promise<void> {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-readme', {
method: 'POST',
body: JSON.stringify({ skillKey, slug, baseDir }),
});
if (!result?.success) {
throw new Error(result?.error || 'Open readme failed');
}
}
export async function apiGetSkillsDir(): Promise<string> {
try {
const result = await hostApiFetch<{ success: boolean; dir?: string; error?: string }>('/api/clawhub/skills-dir');
if (result?.success && result.dir) return result.dir;
} catch {
// fallback
}
return '~/.zn-ai/skills';
}
export async function apiOpenSkillsDir(): Promise<void> {
const dir = await apiGetSkillsDir();
try {
const result = await hostApiFetch<{ success: boolean; error?: string }>('/api/clawhub/open-skills-dir', {
method: 'POST',
body: JSON.stringify({ dir }),
});
if (result?.success) return;
throw new Error(result?.error || 'Open failed');
} catch (error) {
await navigator.clipboard.writeText(dir);
throw new Error('Path copied to clipboard: ' + dir);
}
}

68
src/lib/skills-types.ts Normal file
View File

@@ -0,0 +1,68 @@
/**
* Skill Type Definitions
* Types for skills/plugins
*/
/**
* Skill data structure
*/
export interface Skill {
id: string;
slug?: string;
name: string;
description: string;
enabled: boolean;
icon?: string;
version?: string;
author?: string;
configurable?: boolean;
config?: Record<string, unknown>;
isCore?: boolean;
isBundled?: boolean;
dependencies?: string[];
source?: string;
baseDir?: string;
filePath?: string;
}
/**
* Skill bundle (preset skill collection)
*/
export interface SkillBundle {
id: string;
name: string;
nameZh: string;
description: string;
descriptionZh: string;
icon: string;
skills: string[];
recommended?: boolean;
}
/**
* Marketplace skill data
*/
export interface MarketplaceSkill {
slug: string;
name: string;
description: string;
version: string;
author?: string;
downloads?: number;
stars?: number;
}
/**
* Skill configuration schema
*/
export interface SkillConfigSchema {
type: 'object';
properties: Record<string, {
type: 'string' | 'number' | 'boolean' | 'array';
title?: string;
description?: string;
default?: unknown;
enum?: unknown[];
}>;
required?: string[];
}

19
src/lib/skills-utils.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { Skill } from './skills-types';
export function resolveSkillSourceLabel(
skill: Skill,
t: (key: string, defaultValue?: string) => string
): string {
const source = (skill.source || '').trim().toLowerCase();
if (!source) {
if (skill.isBundled) return t('skills.source.badge.bundled', 'Built-in');
return t('skills.source.badge.unknown', 'Unknown source');
}
if (source === 'openclaw-bundled') return t('skills.source.badge.bundled', 'Built-in');
if (source === 'openclaw-managed') return t('skills.source.badge.managed', 'Managed');
if (source === 'openclaw-workspace') return t('skills.source.badge.workspace', 'Workspace');
if (source === 'openclaw-extra') return t('skills.source.badge.extra', 'Extra dirs');
if (source === 'agents-skills-personal') return t('skills.source.badge.agentsPersonal', 'Personal .agents');
if (source === 'agents-skills-project') return t('skills.source.badge.agentsProject', 'Project .agents');
return source;
}