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>
This commit is contained in:
193
desktop/src/lib/skill-adapter.ts
Normal file
193
desktop/src/lib/skill-adapter.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Skill Adapter - Converts between configStore and UI skill formats
|
||||
*
|
||||
* Bridges the gap between:
|
||||
* - configStore.SkillInfo (backend/Gateway format)
|
||||
* - SkillMarket UI format (based on skill-discovery types)
|
||||
*
|
||||
* Part of Phase 1: Skill Market Store Unification
|
||||
*/
|
||||
|
||||
import type { SkillInfo as ConfigSkillInfo } from '../store/configStore';
|
||||
|
||||
// === UI Skill Types (aligned with SkillMarket expectations) ===
|
||||
|
||||
export interface UISkillInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
triggers: string[];
|
||||
capabilities: string[];
|
||||
toolDeps: string[];
|
||||
installed: boolean;
|
||||
category?: string;
|
||||
path?: string;
|
||||
source?: 'builtin' | 'extra';
|
||||
}
|
||||
|
||||
// Category mapping based on skill keywords
|
||||
const CATEGORY_KEYWORDS: Record<string, string[]> = {
|
||||
development: ['code', 'git', 'frontend', 'backend', 'react', 'vue', 'api', 'typescript', 'javascript'],
|
||||
security: ['security', 'audit', 'vulnerability', 'pentest', 'auth'],
|
||||
analytics: ['data', 'analysis', 'analytics', 'visualization', 'report'],
|
||||
content: ['writing', 'content', 'article', 'copy', 'chinese'],
|
||||
ops: ['devops', 'docker', 'k8s', 'deploy', 'ci', 'cd', 'automation'],
|
||||
management: ['pm', 'project', 'requirement', 'planning', 'prd'],
|
||||
testing: ['test', 'api test', 'e2e', 'unit'],
|
||||
business: ['finance', 'budget', 'expense', 'accounting'],
|
||||
marketing: ['social', 'media', 'marketing', 'campaign', 'operation'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Infer category from skill name and description
|
||||
*/
|
||||
function inferCategory(skill: ConfigSkillInfo): string | undefined {
|
||||
const text = `${skill.name} ${skill.description || ''}`.toLowerCase();
|
||||
|
||||
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
|
||||
if (keywords.some(keyword => text.includes(keyword))) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract trigger patterns from config format
|
||||
*/
|
||||
function extractTriggers(triggers?: ConfigSkillInfo['triggers']): string[] {
|
||||
if (!triggers) return [];
|
||||
|
||||
return triggers
|
||||
.map(t => t.pattern || t.type)
|
||||
.filter((p): p is string => Boolean(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract capabilities from actions
|
||||
*/
|
||||
function extractCapabilities(actions?: ConfigSkillInfo['actions']): string[] {
|
||||
if (!actions) return [];
|
||||
|
||||
return actions
|
||||
.map(a => a.type)
|
||||
.filter((t): t is string => Boolean(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool dependencies from actions params
|
||||
*/
|
||||
function extractToolDeps(actions?: ConfigSkillInfo['actions']): string[] {
|
||||
if (!actions) return [];
|
||||
|
||||
const deps = new Set<string>();
|
||||
|
||||
for (const action of actions) {
|
||||
if (action.params?.tools && Array.isArray(action.params.tools)) {
|
||||
for (const tool of action.params.tools) {
|
||||
if (typeof tool === 'string') {
|
||||
deps.add(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.params?.toolDeps && Array.isArray(action.params.toolDeps)) {
|
||||
for (const dep of action.params.toolDeps) {
|
||||
if (typeof dep === 'string') {
|
||||
deps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(deps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt a single skill from configStore format to UI format
|
||||
*/
|
||||
export function adaptSkillInfo(skill: ConfigSkillInfo): UISkillInfo {
|
||||
return {
|
||||
id: skill.id,
|
||||
name: skill.name,
|
||||
description: skill.description || '',
|
||||
triggers: extractTriggers(skill.triggers),
|
||||
capabilities: extractCapabilities(skill.actions),
|
||||
toolDeps: extractToolDeps(skill.actions),
|
||||
installed: skill.enabled ?? false,
|
||||
category: inferCategory(skill),
|
||||
path: skill.path,
|
||||
source: skill.source,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt an array of skills from configStore format to UI format
|
||||
*/
|
||||
export function adaptSkills(skills: ConfigSkillInfo[]): UISkillInfo[] {
|
||||
return skills.map(adaptSkillInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search skills by query string
|
||||
*/
|
||||
export function searchSkills(skills: UISkillInfo[], query: string): UISkillInfo[] {
|
||||
const q = query.toLowerCase().trim();
|
||||
if (!q) return skills;
|
||||
|
||||
const tokens = q.split(/[\s,;.!?.,;!?]+/).filter(t => t.length > 0);
|
||||
|
||||
const scored = skills.map(skill => {
|
||||
let score = 0;
|
||||
|
||||
// Name match (highest weight)
|
||||
if (skill.name.toLowerCase().includes(q)) score += 10;
|
||||
|
||||
// Description match
|
||||
if (skill.description.toLowerCase().includes(q)) score += 5;
|
||||
|
||||
// Trigger match
|
||||
for (const trigger of skill.triggers) {
|
||||
const tLower = trigger.toLowerCase();
|
||||
if (tLower === q) { score += 15; break; }
|
||||
if (tLower.includes(q) || q.includes(tLower)) score += 8;
|
||||
}
|
||||
|
||||
// Capability match
|
||||
for (const cap of skill.capabilities) {
|
||||
if (cap.toLowerCase().includes(q)) score += 4;
|
||||
}
|
||||
|
||||
// Token-level matching
|
||||
for (const token of tokens) {
|
||||
if (skill.name.toLowerCase().includes(token)) score += 2;
|
||||
if (skill.description.toLowerCase().includes(token)) score += 1;
|
||||
for (const trigger of skill.triggers) {
|
||||
if (trigger.toLowerCase().includes(token)) score += 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Category match
|
||||
if (skill.category && skill.category.toLowerCase().includes(q)) score += 3;
|
||||
|
||||
return { skill, score };
|
||||
});
|
||||
|
||||
return scored
|
||||
.filter(s => s.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(s => s.skill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique categories from skills
|
||||
*/
|
||||
export function getCategories(skills: UISkillInfo[]): string[] {
|
||||
const categories = new Set<string>();
|
||||
for (const skill of skills) {
|
||||
if (skill.category) {
|
||||
categories.add(skill.category);
|
||||
}
|
||||
}
|
||||
return Array.from(categories);
|
||||
}
|
||||
Reference in New Issue
Block a user