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.
This commit is contained in:
DEV_DSW
2026-04-23 11:41:52 +08:00
parent f80bdc7f11
commit 655e7c51d2
16 changed files with 1818 additions and 51 deletions

152
tests/skills-routes.test.ts Normal file
View File

@@ -0,0 +1,152 @@
// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
install: vi.fn(),
getAllSkillConfigs: vi.fn(),
updateSkillConfig: vi.fn(),
openPath: vi.fn(),
MockSkillInstallServiceError: class MockSkillInstallServiceError extends Error {
status: number;
code: string;
constructor(message: string, status: number, code: string) {
super(message);
this.status = status;
this.code = code;
}
},
}));
vi.mock('@service/skill-install-service', () => ({
SkillInstallService: class {
install = mocks.install;
},
SkillInstallServiceError: mocks.MockSkillInstallServiceError,
}));
vi.mock('../electron/utils/skill-config', () => ({
getAllSkillConfigs: mocks.getAllSkillConfigs,
updateSkillConfig: mocks.updateSkillConfig,
}));
vi.mock('../electron/utils/paths', () => ({
getOpenClawConfigDir: () => 'C:/Users/Administrator/.openclaw',
}));
vi.mock('electron', () => ({
shell: {
openPath: mocks.openPath,
},
}));
import { normalizeRequest } from '../electron/api/route-utils';
import { handleSkillRoutes } from '../electron/api/routes/skills';
const ctx = {
gatewayManager: null,
providerApiService: null,
mainWindow: null,
clawHubService: {
getMarketplaceCapability: vi.fn(),
search: vi.fn(),
install: vi.fn(),
uninstall: vi.fn(),
listInstalled: vi.fn(),
openSkillReadme: vi.fn(),
openSkillPath: vi.fn(),
},
} as any;
describe('skill install routes', () => {
beforeEach(() => {
mocks.install.mockReset();
mocks.getAllSkillConfigs.mockReset();
mocks.updateSkillConfig.mockReset();
mocks.openPath.mockReset();
});
it('keeps /api/clawhub/install compatible with the marketplace payload', async () => {
mocks.install.mockResolvedValue({
success: true,
slug: 'demo-skill',
baseDir: 'C:/Users/Administrator/.openclaw/skills/demo-skill',
source: 'marketplace',
enabled: true,
});
const response = await handleSkillRoutes(normalizeRequest({
path: '/api/clawhub/install',
method: 'POST',
body: JSON.stringify({
slug: 'demo-skill',
version: '1.2.3',
force: true,
}),
}), ctx);
expect(mocks.install).toHaveBeenCalledWith({
kind: 'marketplace',
slug: 'demo-skill',
version: '1.2.3',
force: true,
});
expect(response?.ok).toBe(true);
expect(response?.json).toMatchObject({
success: true,
slug: 'demo-skill',
source: 'marketplace',
});
});
it('handles /api/skills/install for github-url payloads', async () => {
mocks.install.mockResolvedValue({
success: true,
slug: 'minimax-xlsx',
baseDir: 'C:/Users/Administrator/.openclaw/skills/minimax-xlsx',
source: 'github-url',
enabled: true,
});
const response = await handleSkillRoutes(normalizeRequest({
path: '/api/skills/install',
method: 'POST',
body: JSON.stringify({
kind: 'github-url',
url: 'https://github.com/MiniMax-AI/skills/blob/main/skills/minimax-xlsx/SKILL.md',
}),
}), ctx);
expect(mocks.install).toHaveBeenCalledWith({
kind: 'github-url',
url: 'https://github.com/MiniMax-AI/skills/blob/main/skills/minimax-xlsx/SKILL.md',
});
expect(response?.ok).toBe(true);
expect(response?.json).toMatchObject({
success: true,
slug: 'minimax-xlsx',
source: 'github-url',
});
});
it('returns the service status code when install validation fails', async () => {
mocks.install.mockRejectedValue(new mocks.MockSkillInstallServiceError(
'GitHub skill URL is invalid.',
400,
'invalid_github_url',
));
const response = await handleSkillRoutes(normalizeRequest({
path: '/api/skills/install',
method: 'POST',
body: JSON.stringify({
kind: 'github-url',
url: 'https://example.com/not-supported',
}),
}), ctx);
expect(response?.ok).toBe(false);
expect(response?.status).toBe(400);
expect(response?.error).toBe('GitHub skill URL is invalid.');
});
});