Files
getskills/index.js
zlei9 4bbdbfd52e feat: 初始化 getskills 技能
添加 SKILL.md 文件,将 getskills 工具转换为标准技能格式

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-22 22:46:32 +08:00

667 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { exec } = require('child_process');
/**
* 基础 URL 配置
* 可以通过环境变量 GETSKILL_BASE_URL 自定义
*/
let BASE_URL = process.env.GETSKILL_BASE_URL || 'https://getskills.certer';
/**
* 设置自定义 BASE_URL
* @param {string} url - 自定义的基础 URL
*/
function setBaseUrl(url) {
BASE_URL = url.replace(/\/$/, ''); // 移除末尾的斜杠
console.log(`BASE_URL 已设置为: ${BASE_URL}`);
}
/**
* 获取当前 BASE_URL
* @returns {string} 当前的基础 URL
*/
function getBaseUrl() {
return BASE_URL;
}
/**
* 检查 Git 是否已安装
*/
async function checkGitInstalled() {
try {
await executeCommand('git --version');
return true;
} catch (error) {
return false;
}
}
/**
* 获取 Git 下载链接(根据操作系统)
*/
function getGitDownloadUrl() {
const platform = os.platform();
const arch = os.arch();
switch (platform) {
case 'win32':
// Windows
if (arch === 'x64') {
return 'https://github.com/git-for-windows/git/releases/download/v2.44.0.windows.1/Git-2.44.0-64-bit.exe';
} else {
return 'https://github.com/git-for-windows/git/releases/download/v2.44.0.windows.1/Git-2.44.0-32-bit.exe';
}
case 'darwin':
// macOS - 提示使用 Homebrew
return 'homebrew'; // 特殊标记
case 'linux':
// Linux - 提示使用包管理器
return 'package-manager'; // 特殊标记
default:
return null;
}
}
/**
* 下载文件
*/
function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const protocol = urlObj.protocol === 'https:' ? https : http;
const file = fs.createWriteStream(destPath);
protocol.get(url, (response) => {
// 处理重定向
if (response.statusCode === 301 || response.statusCode === 302) {
file.close();
fs.unlinkSync(destPath);
return downloadFile(response.headers.location, destPath)
.then(resolve)
.catch(reject);
}
if (response.statusCode !== 200) {
file.close();
fs.unlinkSync(destPath);
return reject(new Error(`下载失败,状态码: ${response.statusCode}`));
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const percent = ((downloadedSize / totalSize) * 100).toFixed(2);
process.stdout.write(`\r下载进度: ${percent}%`);
});
response.pipe(file);
file.on('finish', () => {
file.close();
console.log('\n下载完成');
resolve(destPath);
});
}).on('error', (err) => {
file.close();
fs.unlinkSync(destPath);
reject(err);
});
});
}
/**
* 安装 Git
*/
async function installGit() {
const platform = os.platform();
const downloadUrl = getGitDownloadUrl();
console.log('检测到系统未安装 Git正在准备安装...\n');
if (downloadUrl === 'homebrew') {
console.log('macOS 系统检测到未安装 Git');
console.log('请使用以下命令安装 Git:\n');
console.log(' brew install git');
console.log('\n如果未安装 Homebrew请访问: https://brew.sh/');
throw new Error('请先安装 Git 后再运行此命令');
}
if (downloadUrl === 'package-manager') {
console.log('Linux 系统检测到未安装 Git');
console.log('请使用系统包管理器安装 Git:\n');
console.log(' Ubuntu/Debian: sudo apt-get install git');
console.log(' CentOS/RHEL: sudo yum install git');
console.log(' Fedora: sudo dnf install git');
console.log(' Arch: sudo pacman -S git');
throw new Error('请先安装 Git 后再运行此命令');
}
if (platform === 'win32') {
console.log('Windows 系统检测到未安装 Git');
console.log('正在下载 Git 安装程序...\n');
const tempDir = os.tmpdir();
const installerPath = path.join(tempDir, 'git-installer.exe');
try {
await downloadFile(downloadUrl, installerPath);
console.log(`\nGit 安装程序已下载到: ${installerPath}`);
console.log('\n正在启动安装程序请按照提示完成安装...');
console.log('安装完成后,请重新运行此命令。\n');
// 启动安装程序
exec(`"${installerPath}"`, (error) => {
if (error) {
console.error('启动安装程序失败:', error.message);
}
});
throw new Error('请完成 Git 安装后重新运行此命令');
} catch (error) {
console.error('\n下载 Git 安装程序失败:', error.message);
console.log('\n您可以手动访问以下网址下载 Git:');
console.log('https://git-scm.com/download/win');
throw new Error('请先安装 Git 后再运行此命令');
}
}
}
/**
* 确保 Git 已安装
*/
async function ensureGitInstalled() {
const isInstalled = await checkGitInstalled();
if (!isInstalled) {
await installGit();
}
return true;
}
/**
* 获取 OpenClaw skills 目录路径(根据操作系统)
*/
function getSkillsDirectory() {
const platform = os.platform();
const homeDir = os.homedir();
switch (platform) {
case 'win32':
// Windows: %USERPROFILE%\.claude\skills
return path.join(homeDir, '.claude', 'skills');
case 'darwin':
// macOS: ~/.claude/skills
return path.join(homeDir, '.claude', 'skills');
case 'linux':
// Linux: ~/.claude/skills
return path.join(homeDir, '.claude', 'skills');
default:
// 默认使用 ~/.claude/skills
return path.join(homeDir, '.claude', 'skills');
}
}
/**
* 确保目录存在
*/
function ensureDirectoryExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* HTTP/HTTPS 请求封装
*/
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const protocol = urlObj.protocol === 'https:' ? https : http;
const reqOptions = {
hostname: urlObj.hostname,
port: urlObj.port,
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = protocol.request(reqOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: data
});
} else {
reject(new Error(`Request failed with status code ${res.statusCode}`));
}
});
});
req.on('error', (err) => {
reject(err);
});
if (options.body) {
req.write(options.body);
}
req.end();
});
}
/**
* 执行命令
*/
function executeCommand(command, cwd) {
return new Promise((resolve, reject) => {
exec(command, { cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
});
});
}
/**
* 搜索技能(从配置的 BASE_URL API
* @param {string} keyword - 搜索关键词
* @param {Object} options - 搜索选项
* @param {string} options.sort - 排序字段,默认 'updated'
* @param {string} options.order - 排序顺序,默认 'desc'
* @param {number} options.page - 页码,默认 1
* @param {number} options.limit - 每页数量,默认 20
* @returns {Promise<Array>} 技能列表(包含 git 地址)
*/
async function searchSkills(keyword, options = {}) {
const {
sort = 'updated',
order = 'desc',
page = 1,
limit = 20
} = options;
const url = `${BASE_URL}/repo/search?sort=${sort}&order=${order}&q=${encodeURIComponent(keyword)}&page=${page}&limit=${limit}`;
try {
const response = await request(url);
const result = JSON.parse(response.body);
// 解析返回格式: { ok: true, data: [{ repository: {...} }] }
if (!result.ok || !result.data) {
return [];
}
// 提取需要的字段
return result.data.map(item => {
const repo = item.repository || {};
return {
name: repo.full_name || '',
full_name: repo.full_name || '',
description: repo.description || '',
html_url: repo.html_url || '',
git_url: repo.html_url ? `${repo.html_url}.git` : '',
clone_url: repo.clone_url || ''
};
});
} catch (error) {
console.error('搜索技能失败:', error.message);
throw error;
}
}
/**
* 获取技能详情(包含 git 地址)
* @param {string} skillId - 技能ID或名称
* @returns {Promise<Object>} 技能详情
*/
async function getSkillDetail(skillId) {
const url = `${BASE_URL}/repo/${encodeURIComponent(skillId)}`;
try {
const response = await request(url);
const result = JSON.parse(response.body);
// 如果返回的数据格式类似搜索接口,需要解析 repository
if (result.repository) {
const repo = result.repository;
return {
name: repo.full_name || repo.name || skillId,
full_name: repo.full_name || '',
description: repo.description || '',
html_url: repo.html_url || '',
git_url: repo.html_url ? `${repo.html_url}.git` : '',
clone_url: repo.clone_url || ''
};
}
// 如果是直接返回数据(向后兼容)
return {
name: result.full_name || result.name || skillId,
full_name: result.full_name || '',
description: result.description || '',
html_url: result.html_url || '',
git_url: result.html_url ? `${result.html_url}.git` : (result.git_url || ''),
clone_url: result.clone_url || ''
};
} catch (error) {
console.error('获取技能详情失败:', error.message);
throw error;
}
}
/**
* 获取技能仓库缓存目录
*/
function getSkillsCacheDirectory() {
const homeDir = os.homedir();
return path.join(homeDir, '.claude', 'skills-cache');
}
/**
* 克隆或更新 Git 仓库
* @param {string} gitUrl - Git 仓库地址
* @param {string} skillName - 技能名称
* @returns {Promise<string>} 本地仓库路径
*/
async function cloneOrUpdateRepo(gitUrl, skillName) {
// 首先检查并确保 Git 已安装
await ensureGitInstalled();
const cacheDir = getSkillsCacheDirectory();
ensureDirectoryExists(cacheDir);
const repoPath = path.join(cacheDir, skillName);
if (fs.existsSync(repoPath)) {
// 仓库已存在,执行 git pull
console.log(`更新仓库: ${skillName}...`);
try {
await executeCommand('git pull', repoPath);
console.log(`仓库更新成功: ${repoPath}`);
} catch (error) {
console.error(`更新失败: ${error.message}`);
throw error;
}
} else {
// 克隆新仓库
console.log(`克隆仓库: ${gitUrl}...`);
try {
await executeCommand(`git clone "${gitUrl}" "${skillName}"`, cacheDir);
console.log(`仓库克隆成功: ${repoPath}`);
} catch (error) {
console.error(`克隆失败: ${error.message}`);
throw error;
}
}
return repoPath;
}
/**
* 从仓库复制技能文件到 skills 目录
* @param {string} repoPath - 仓库路径
* @param {string} skillName - 技能名称
*/
function copySkillFiles(repoPath) {
const skillsDir = getSkillsDirectory();
ensureDirectoryExists(skillsDir);
// 查找仓库中的 .md 文件
const files = fs.readdirSync(repoPath);
const mdFiles = files.filter(file =>
file.endsWith('.md') &&
!file.toLowerCase().startsWith('readme')
);
if (mdFiles.length === 0) {
throw new Error(`在仓库中未找到技能 .md 文件`);
}
const copiedFiles = [];
mdFiles.forEach(file => {
const sourcePath = path.join(repoPath, file);
const targetPath = path.join(skillsDir, file);
fs.copyFileSync(sourcePath, targetPath);
console.log(`已复制: ${file} -> ${targetPath}`);
copiedFiles.push(targetPath);
});
return copiedFiles;
}
/**
* 下载技能(通过 Git
* @param {string} skillIdOrName - 技能ID或名称
*/
async function downloadSkill(skillIdOrName) {
try {
console.log(`获取技能信息: ${skillIdOrName}...`);
const skillDetail = await getSkillDetail(skillIdOrName);
// 支持 clone_url 或 git_url
const gitUrl = skillDetail.clone_url || skillDetail.git_url;
if (!gitUrl) {
throw new Error('技能信息中未包含 git_url 或 clone_url');
}
const skillName = skillDetail.name || skillIdOrName;
const repoPath = await cloneOrUpdateRepo(gitUrl, skillName);
const copiedFiles = copySkillFiles(repoPath);
return {
skillName,
repoPath,
files: copiedFiles
};
} catch (error) {
console.error(`下载技能失败: ${error.message}`);
throw error;
}
}
/**
* 更新技能(通过 Git
* @param {string} skillName - 技能名称
*/
async function updateSkill(skillName) {
try {
console.log(`正在更新技能: ${skillName}...`);
const cacheDir = getSkillsCacheDirectory();
const repoPath = path.join(cacheDir, skillName);
if (!fs.existsSync(repoPath)) {
console.log(`技能仓库不存在,将重新下载...`);
return await downloadSkill(skillName);
}
// 执行 git pull
await executeCommand('git pull', repoPath);
console.log(`Git 仓库已更新`);
// 复制文件到 skills 目录
const copiedFiles = copySkillFiles(repoPath);
console.log(`技能 ${skillName} 更新成功!`);
return {
skillName,
repoPath,
files: copiedFiles
};
} catch (error) {
console.error(`更新技能 ${skillName} 失败:`, error.message);
throw error;
}
}
/**
* 列出本地已安装的技能
*/
function listLocalSkills() {
const skillsDir = getSkillsDirectory();
if (!fs.existsSync(skillsDir)) {
console.log('技能目录不存在');
return [];
}
const files = fs.readdirSync(skillsDir);
const skillFiles = files.filter(file => file.endsWith('.md'));
return skillFiles;
}
/**
* 命令行接口
*/
async function cli() {
const args = process.argv.slice(2);
const command = args[0];
try {
switch (command) {
case 'search':
if (!args[1]) {
console.error('请提供搜索关键词');
console.log('用法: getskill search <关键词>');
process.exit(1);
}
const results = await searchSkills(args[1]);
console.log(`找到 ${results.length} 个技能:\n`);
results.forEach((skill, index) => {
console.log(`${index + 1}. ${skill.full_name}`);
if (skill.description) {
console.log(` 描述: ${skill.description}`);
}
if (skill.git_url) {
console.log(` Git: ${skill.git_url}`);
}
console.log('');
});
break;
case 'install':
case 'get':
case 'download':
if (!args[1]) {
console.error('请提供技能ID或名称');
console.log('用法: getskill install <技能名称>');
process.exit(1);
}
const result = await downloadSkill(args[1]);
console.log(`\n技能已安装到 skills 目录:`);
result.files.forEach(file => console.log(` - ${file}`));
console.log(`\nGit 仓库缓存: ${result.repoPath}`);
break;
case 'update':
if (!args[1]) {
console.error('请提供技能名称');
console.log('用法: getskill update <技能名称>');
process.exit(1);
}
const updateResult = await updateSkill(args[1]);
console.log(`\n技能已更新到 skills 目录:`);
updateResult.files.forEach(file => console.log(` - ${file}`));
break;
case 'list':
const skills = listLocalSkills();
console.log(`本地已安装的技能 (${skills.length}):`);
skills.forEach((skill, index) => {
console.log(`${index + 1}. ${skill}`);
});
break;
case 'path':
console.log(`技能目录: ${getSkillsDirectory()}`);
console.log(`缓存目录: ${getSkillsCacheDirectory()}`);
break;
case 'config':
if (args[1] === 'set' && args[2]) {
setBaseUrl(args[2]);
} else if (args[1] === 'get') {
console.log(`当前 BASE_URL: ${getBaseUrl()}`);
} else {
console.log('用法:');
console.log(' getskill config set <URL> - 设置自定义 API 地址');
console.log(' getskill config get - 查看当前 API 地址');
}
break;
case 'clean':
const cacheDir = getSkillsCacheDirectory();
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true, force: true });
console.log(`已清理缓存目录: ${cacheDir}`);
} else {
console.log('缓存目录不存在');
}
break;
default:
console.log('GetSkill - OpenClaw 技能管理工具');
console.log('');
console.log('用法:');
console.log(' getskill search <关键词> - 从 API 搜索技能');
console.log(' getskill install <技能名称> - 通过 git clone 安装技能');
console.log(' getskill update <技能名称> - 通过 git pull 更新技能');
console.log(' getskill list - 列出本地技能');
console.log(' getskill path - 显示目录路径');
console.log(' getskill config set <URL> - 设置自定义 API 地址');
console.log(' getskill config get - 查看当前 API 地址');
console.log(' getskill clean - 清理 git 缓存');
console.log('');
console.log(`当前 API: ${getBaseUrl()}`);
console.log(`技能目录: ${getSkillsDirectory()}`);
console.log(`缓存目录: ${getSkillsCacheDirectory()}`);
}
} catch (error) {
console.error('执行命令时出错:', error.message);
process.exit(1);
}
}
// 导出模块函数
module.exports = {
searchSkills,
getSkillDetail,
downloadSkill,
updateSkill,
listLocalSkills,
getSkillsDirectory,
getSkillsCacheDirectory,
cloneOrUpdateRepo,
copySkillFiles,
checkGitInstalled,
ensureGitInstalled,
setBaseUrl,
getBaseUrl
};
// 如果直接运行此文件,执行 CLI
if (require.main === module) {
cli();
}