mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
feat: add Skill Hub feature for managing community skills
- Implemented Skill Hub page with tabs for remote, local, and installed skills. - Added localization support for Chinese in skill-hub.json. - Created API routes for fetching remote skills, listing local skills, and managing installed skills. - Developed functionality for installing and uninstalling skills from both remote and local sources. - Introduced caching mechanism for remote skills and handling updates for installed skills.
This commit is contained in:
@@ -352,3 +352,32 @@ export type {
|
||||
UseUpdateRerankerConfigReturn,
|
||||
UseCcwToolsListReturn,
|
||||
} from './useCodexLens';
|
||||
|
||||
// ========== Skill Hub ==========
|
||||
export {
|
||||
useRemoteSkills,
|
||||
useLocalSkills,
|
||||
useInstalledSkills,
|
||||
useSkillHubUpdates,
|
||||
useInstallSkill,
|
||||
useCacheSkill,
|
||||
useUninstallSkill,
|
||||
useSkillHubStats,
|
||||
useSkillHub,
|
||||
skillHubKeys,
|
||||
} from './useSkillHub';
|
||||
export type {
|
||||
CliType,
|
||||
SkillSource,
|
||||
RemoteSkill,
|
||||
LocalSkill,
|
||||
InstalledSkill,
|
||||
RemoteSkillsResponse,
|
||||
LocalSkillsResponse,
|
||||
InstalledSkillsResponse,
|
||||
SkillInstallRequest,
|
||||
SkillInstallResponse,
|
||||
SkillCacheRequest,
|
||||
SkillCacheResponse,
|
||||
SkillHubStats,
|
||||
} from './useSkillHub';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useExecutionMonitorStore } from '@/stores/executionMonitorStore';
|
||||
import { useSessionManagerStore } from '@/stores/sessionManagerStore';
|
||||
import { toast } from '@/stores/notificationStore';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
@@ -54,7 +53,6 @@ export function useExecuteFlowInSession() {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const handleExecutionMessage = useExecutionMonitorStore((s) => s.handleExecutionMessage);
|
||||
const setPanelOpen = useExecutionMonitorStore((s) => s.setPanelOpen);
|
||||
const lockSession = useSessionManagerStore((s) => s.lockSession);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (params: {
|
||||
@@ -89,8 +87,8 @@ export function useExecuteFlowInSession() {
|
||||
},
|
||||
});
|
||||
|
||||
// Lock the session
|
||||
lockSession(sessionKey, `Executing workflow: ${flowId}`, executionId);
|
||||
// Note: Session locking is handled by backend WebSocket broadcast (CLI_SESSION_LOCKED)
|
||||
// Frontend sessionManagerStore updates state via WebSocket message handler
|
||||
|
||||
// Open the execution monitor panel
|
||||
setPanelOpen(true);
|
||||
|
||||
418
ccw/frontend/src/hooks/useSkillHub.ts
Normal file
418
ccw/frontend/src/hooks/useSkillHub.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
// ========================================
|
||||
// Skill Hub Hooks
|
||||
// ========================================
|
||||
// React Query hooks for Skill Hub API
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// ============================================================================
|
||||
// API Helper
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get CSRF token from cookie
|
||||
*/
|
||||
function getCsrfToken(): string | null {
|
||||
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed fetch wrapper with CSRF token handling
|
||||
*/
|
||||
async function fetchApi<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
// Add CSRF token for mutating requests
|
||||
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
|
||||
const csrfToken = getCsrfToken();
|
||||
if (csrfToken) {
|
||||
headers.set('X-CSRF-Token', csrfToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Set content type for JSON requests
|
||||
if (options.body && typeof options.body === 'string') {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error: { message: string; status: number; code?: string } = {
|
||||
message: response.statusText || 'Request failed',
|
||||
status: response.status,
|
||||
};
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
try {
|
||||
const body = await response.json();
|
||||
if (body.message) error.message = body.message;
|
||||
else if (body.error) error.message = body.error;
|
||||
if (body.code) error.code = body.code;
|
||||
} catch {
|
||||
// Silently ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle no-content responses
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type CliType = 'claude' | 'codex';
|
||||
export type SkillSource = 'remote' | 'local';
|
||||
|
||||
export interface RemoteSkill {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
author: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
downloadUrl: string;
|
||||
readmeUrl?: string;
|
||||
homepage?: string;
|
||||
license?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface LocalSkill {
|
||||
id: string;
|
||||
name: string;
|
||||
folderName: string;
|
||||
description: string;
|
||||
version: string;
|
||||
author?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
path: string;
|
||||
source: 'local';
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface InstalledSkill {
|
||||
id: string;
|
||||
name: string;
|
||||
folderName: string;
|
||||
version: string;
|
||||
installedAt: string;
|
||||
installedTo: CliType;
|
||||
source: SkillSource;
|
||||
originalId: string;
|
||||
updatesAvailable?: boolean;
|
||||
latestVersion?: string;
|
||||
}
|
||||
|
||||
export interface RemoteSkillsResponse {
|
||||
success: boolean;
|
||||
data: RemoteSkill[];
|
||||
meta: {
|
||||
version: string;
|
||||
updated_at: string;
|
||||
source: 'github' | 'http' | 'local';
|
||||
};
|
||||
total: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface LocalSkillsResponse {
|
||||
success: boolean;
|
||||
data: LocalSkill[];
|
||||
total: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface InstalledSkillsResponse {
|
||||
success: boolean;
|
||||
data: InstalledSkill[];
|
||||
total: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface SkillInstallRequest {
|
||||
skillId: string;
|
||||
cliType: CliType;
|
||||
source: SkillSource;
|
||||
customName?: string;
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export interface SkillInstallResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
installedPath?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SkillCacheRequest {
|
||||
skillId: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
export interface SkillCacheResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SkillHubStats {
|
||||
remoteTotal: number;
|
||||
localTotal: number;
|
||||
installedTotal: number;
|
||||
updatesAvailable: number;
|
||||
claudeInstalled: number;
|
||||
codexInstalled: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Keys
|
||||
// ============================================================================
|
||||
|
||||
export const skillHubKeys = {
|
||||
all: ['skill-hub'] as const,
|
||||
remote: () => [...skillHubKeys.all, 'remote'] as const,
|
||||
local: () => [...skillHubKeys.all, 'local'] as const,
|
||||
installed: (checkUpdates?: boolean) => [...skillHubKeys.all, 'installed', checkUpdates] as const,
|
||||
updates: () => [...skillHubKeys.all, 'updates'] as const,
|
||||
stats: () => [...skillHubKeys.all, 'stats'] as const,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Remote Skills Hook
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch remote skills from GitHub/HTTP index
|
||||
*/
|
||||
export function useRemoteSkills(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: skillHubKeys.remote(),
|
||||
queryFn: () => fetchApi<RemoteSkillsResponse>('/api/skill-hub/remote'),
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => ({
|
||||
skills: data.data,
|
||||
meta: data.meta,
|
||||
total: data.total,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Local Skills Hook
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch local shared skills
|
||||
*/
|
||||
export function useLocalSkills(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: skillHubKeys.local(),
|
||||
queryFn: () => fetchApi<LocalSkillsResponse>('/api/skill-hub/local'),
|
||||
enabled,
|
||||
select: (data) => ({
|
||||
skills: data.data,
|
||||
total: data.total,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Installed Skills Hook
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch installed skills from hub
|
||||
*/
|
||||
export function useInstalledSkills(options?: { checkUpdates?: boolean; enabled?: boolean }) {
|
||||
const { checkUpdates = false, enabled = true } = options || {};
|
||||
|
||||
return useQuery({
|
||||
queryKey: skillHubKeys.installed(checkUpdates),
|
||||
queryFn: () => {
|
||||
const url = checkUpdates
|
||||
? '/api/skill-hub/installed?checkUpdates=true'
|
||||
: '/api/skill-hub/installed';
|
||||
return fetchApi<InstalledSkillsResponse>(url);
|
||||
},
|
||||
enabled,
|
||||
select: (data) => ({
|
||||
skills: data.data,
|
||||
total: data.total,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Updates Check Hook
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check for available updates
|
||||
*/
|
||||
export function useSkillHubUpdates(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: skillHubKeys.updates(),
|
||||
queryFn: () => fetchApi<InstalledSkillsResponse>('/api/skill-hub/updates'),
|
||||
enabled,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
select: (data) => ({
|
||||
updates: data.data,
|
||||
total: data.total,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Install Skill Mutation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Install skill mutation
|
||||
*/
|
||||
export function useInstallSkill() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (request: SkillInstallRequest) =>
|
||||
fetchApi<SkillInstallResponse>('/api/skill-hub/install', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: skillHubKeys.installed() });
|
||||
queryClient.invalidateQueries({ queryKey: skillHubKeys.updates() });
|
||||
queryClient.invalidateQueries({ queryKey: skillHubKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cache Skill Mutation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Cache remote skill mutation
|
||||
*/
|
||||
export function useCacheSkill() {
|
||||
return useMutation({
|
||||
mutationFn: (request: SkillCacheRequest) =>
|
||||
fetchApi<SkillCacheResponse>('/api/skill-hub/cache', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Uninstall Skill Mutation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Uninstall skill mutation
|
||||
*/
|
||||
export function useUninstallSkill() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ skillId, cliType }: { skillId: string; cliType: CliType }) =>
|
||||
fetchApi<{ success: boolean; message: string }>(`/api/skill-hub/installed/${skillId}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ cliType }),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
// Invalidate relevant queries
|
||||
queryClient.invalidateQueries({ queryKey: skillHubKeys.installed() });
|
||||
queryClient.invalidateQueries({ queryKey: skillHubKeys.updates() });
|
||||
queryClient.invalidateQueries({ queryKey: skillHubKeys.stats() });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stats Hook
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get skill hub statistics
|
||||
* Combines data from multiple endpoints
|
||||
*/
|
||||
export function useSkillHubStats(enabled = true) {
|
||||
const { data: remoteData } = useRemoteSkills(enabled);
|
||||
const { data: localData } = useLocalSkills(enabled);
|
||||
const { data: installedData } = useInstalledSkills({ checkUpdates: true, enabled });
|
||||
|
||||
return useQuery({
|
||||
queryKey: skillHubKeys.stats(),
|
||||
queryFn: (): SkillHubStats => {
|
||||
const installed = installedData?.skills || [];
|
||||
const updatesAvailable = installed.filter(s => s.updatesAvailable).length;
|
||||
const claudeInstalled = installed.filter(s => s.installedTo === 'claude').length;
|
||||
const codexInstalled = installed.filter(s => s.installedTo === 'codex').length;
|
||||
|
||||
return {
|
||||
remoteTotal: remoteData?.total || 0,
|
||||
localTotal: localData?.total || 0,
|
||||
installedTotal: installed.length,
|
||||
updatesAvailable,
|
||||
claudeInstalled,
|
||||
codexInstalled,
|
||||
};
|
||||
},
|
||||
enabled: enabled && !!remoteData && !!localData && !!installedData,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Combined Hooks
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Combined hook for all skill hub data
|
||||
*/
|
||||
export function useSkillHub(enabled = true) {
|
||||
const remote = useRemoteSkills(enabled);
|
||||
const local = useLocalSkills(enabled);
|
||||
const installed = useInstalledSkills({ checkUpdates: true, enabled });
|
||||
const stats = useSkillHubStats(enabled && remote.isSuccess && local.isSuccess && installed.isSuccess);
|
||||
|
||||
const isLoading = remote.isLoading || local.isLoading || installed.isLoading;
|
||||
const isError = remote.isError || local.isError || installed.isError;
|
||||
const isFetching = remote.isFetching || local.isFetching || installed.isFetching;
|
||||
|
||||
return {
|
||||
remote,
|
||||
local,
|
||||
installed,
|
||||
stats,
|
||||
isLoading,
|
||||
isError,
|
||||
isFetching,
|
||||
refetchAll: () => {
|
||||
remote.refetch();
|
||||
local.refetch();
|
||||
installed.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user