feat: 新增技能相关功能
This commit is contained in:
174
src/lib/skills-api.ts
Normal file
174
src/lib/skills-api.ts
Normal 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
68
src/lib/skills-types.ts
Normal 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
19
src/lib/skills-utils.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user