Files
zn-ai/electron/api/routes/skills.ts
DEV_DSW 655e7c51d2 feat: add GitHub skill installation support
- Implemented functionality to install skills from GitHub URLs.
- Updated API to handle new installation requests from GitHub.
- Enhanced UI to allow users to input GitHub skill URLs for installation.
- Added translations for new GitHub installation features in English, Thai, and Chinese.
- Created tests for the new skill installation service and API routes to ensure proper functionality.
2026-04-23 11:41:52 +08:00

180 lines
5.5 KiB
TypeScript

import type { HostApiResult } from '@src/types/runtime';
import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils';
import { fail, ok, parseJsonBody } from '../route-utils';
import { getAllSkillConfigs, updateSkillConfig } from '../../utils/skill-config';
import { shell } from 'electron';
import { getOpenClawConfigDir } from '../../utils/paths';
import {
SkillInstallService,
SkillInstallServiceError,
type SkillInstallRequest,
} from '@service/skill-install-service';
export async function handleSkillRoutes(
request: NormalizedHostApiRequest,
ctx: HostApiContext,
): Promise<HostApiResult<unknown> | null> {
const { pathname, method } = request;
const installService = new SkillInstallService({ clawHubService: ctx.clawHubService });
if (pathname === '/api/skills/configs' && method === 'GET') {
try {
return ok(await getAllSkillConfigs());
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/skills/config' && method === 'PUT') {
try {
const body = parseJsonBody<{
skillKey: string;
apiKey?: string;
env?: Record<string, string>;
enabled?: boolean;
}>(request.body);
if (!body?.skillKey || !String(body.skillKey).trim()) {
return fail(400, 'skillKey is required');
}
const result = await updateSkillConfig(body.skillKey, {
apiKey: body.apiKey,
env: body.env,
enabled: body.enabled,
});
return ok(result);
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/capability' && method === 'GET') {
try {
return ok({
success: true,
capability: await ctx.clawHubService.getMarketplaceCapability(),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/search' && method === 'POST') {
try {
const body = parseJsonBody<Record<string, unknown>>(request.body);
return ok({
success: true,
results: await ctx.clawHubService.search(body as { query: string; limit?: number }),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/skills/install' && method === 'POST') {
try {
const body = parseJsonBody<SkillInstallRequest>(request.body);
return ok(await installService.install(body));
} catch (error) {
return failInstall(error);
}
}
if (pathname === '/api/clawhub/install' && method === 'POST') {
try {
const body = parseJsonBody<{ slug: string; version?: string; force?: boolean }>(request.body);
return ok(await installService.install({
kind: 'marketplace',
slug: body.slug,
version: body.version,
force: body.force,
}));
} catch (error) {
return failInstall(error);
}
}
if (pathname === '/api/clawhub/uninstall' && method === 'POST') {
try {
const body = parseJsonBody<Record<string, unknown>>(request.body);
await ctx.clawHubService.uninstall(body as { slug: string });
return ok({ success: true });
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/list' && method === 'GET') {
try {
return ok({
success: true,
results: await ctx.clawHubService.listInstalled(),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/open-readme' && method === 'POST') {
try {
const body = parseJsonBody<{ slug?: string; skillKey?: string; baseDir?: string }>(request.body);
await ctx.clawHubService.openSkillReadme(
body.skillKey || body.slug || '',
body.slug,
body.baseDir,
);
return ok({ success: true });
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/open-path' && method === 'POST') {
try {
const body = parseJsonBody<{ slug?: string; skillKey?: string; baseDir?: string }>(request.body);
await ctx.clawHubService.openSkillPath(
body.skillKey || body.slug || '',
body.slug,
body.baseDir,
);
return ok({ success: true });
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/skills-dir' && method === 'GET') {
try {
return ok({
success: true,
path: `${getOpenClawConfigDir()}/skills`,
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/clawhub/open-skills-dir' && method === 'POST') {
try {
const skillsDir = `${getOpenClawConfigDir()}/skills`;
const openResult = await shell.openPath(skillsDir);
if (openResult) {
return fail(500, openResult);
}
return ok({ success: true });
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
return null;
}
function failInstall(error: unknown): HostApiResult<unknown> {
if (error instanceof SkillInstallServiceError) {
return fail(error.status, error.message);
}
return fail(500, error instanceof Error ? error.message : String(error));
}