#!/usr/bin/env node /** * OpenClaw Skills CDN 上传脚本 * * 功能:将所有 skills 上传到七牛云 CDN,生成下载 URL * * 使用方法: * node scripts/upload-skills.js * * 前置条件: * 1. npm install qiniu * 2. 配置下方 QINIU_ACCESS_KEY 和 QINIU_SECRET_KEY * * 参考:hongben-server/modules/fmode-tikhub-server/upload.sh */ const qiniu = require('qiniu'); const fs = require('fs'); const path = require('path'); // ============================================ // 七牛云配置(需要填入你的 AccessKey / SecretKey) // ============================================ const QINIU_ACCESS_KEY = process.env.QINIU_AK || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE'; const QINIU_SECRET_KEY = process.env.QINIU_SK || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX'; const BUCKET = 'nova-repos'; const CDN_DOMAIN = 'https://repos.fmode.cn'; const CDN_PREFIX = 'x/openclaw-skills'; // Skills 源目录(部署目录) const SKILLS_DIR = path.resolve(__dirname, '..', 'workshop'); const DEPLOYED_SKILLS_DIR = path.join(process.env.USERPROFILE || process.env.HOME, '.openclaw', 'skills'); // 要上传的技能目录列表(如果为空,则上传所有 skills) const SKILL_FILTER = []; // 例如 ['tiktok-video-search', 'jimeng-img-v4'] 指定只上传某些 // ============================================ // 上传逻辑 // ============================================ function getUploadToken(bucket, key) { const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY); const putPolicy = new qiniu.rs.PutPolicy({ scope: `${bucket}:${key}`, // 覆盖上传 expires: 3600 }); return putPolicy.uploadToken(mac); } function uploadFile(localFile, cdnKey) { return new Promise((resolve, reject) => { const token = getUploadToken(BUCKET, cdnKey); const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 // 华南(nova-repos 所在区域) })); const putExtra = new qiniu.form_up.PutExtra(); formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => { if (err) { reject(err); return; } if (info.statusCode === 200) { resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` }); } else { reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`)); } }); }); } async function collectSkills() { const skills = []; const dir = DEPLOYED_SKILLS_DIR; if (!fs.existsSync(dir)) { console.error(`Skills 目录不存在: ${dir}`); process.exit(1); } const entries = fs.readdirSync(dir).filter(d => { const fullPath = path.join(dir, d); if (!fs.statSync(fullPath).isDirectory()) return false; if (SKILL_FILTER.length > 0 && !SKILL_FILTER.includes(d)) return false; // 至少有 SKILL.md return fs.existsSync(path.join(fullPath, 'SKILL.md')); }); for (const skillName of entries) { const skillDir = path.join(dir, skillName); const files = []; for (const fileName of ['SKILL.md', 'api-config.json']) { const filePath = path.join(skillDir, fileName); if (fs.existsSync(filePath)) { files.push({ localPath: filePath, cdnKey: `${CDN_PREFIX}/${skillName}/${fileName}`, fileName }); } } if (files.length > 0) { skills.push({ name: skillName, files }); } } return skills; } async function main() { console.log('=================================================='); console.log(' OpenClaw Skills CDN 上传脚本'); console.log('=================================================='); console.log(''); // 检查凭证 if (QINIU_ACCESS_KEY === 'YOUR_ACCESS_KEY_HERE' || QINIU_SECRET_KEY === 'YOUR_SECRET_KEY_HERE') { console.error('❌ 请先配置七牛云凭证!'); console.error(' 方式1: 编辑脚本中的 QINIU_ACCESS_KEY / QINIU_SECRET_KEY'); console.error(' 方式2: 设置环境变量 QINIU_AK / QINIU_SK'); console.error(''); console.error(' 示例: set QINIU_AK=xxx & set QINIU_SK=yyy & node scripts/upload-skills.js'); process.exit(1); } // 收集技能 const skills = await collectSkills(); console.log(`📦 找到 ${skills.length} 个技能待上传`); console.log(`📂 源目录: ${DEPLOYED_SKILLS_DIR}`); console.log(`☁️ Bucket: ${BUCKET}`); console.log(`🔗 CDN前缀: ${CDN_PREFIX}`); console.log(''); // 汇总文件 let totalFiles = 0; skills.forEach(s => { totalFiles += s.files.length; }); console.log(`📄 总计 ${totalFiles} 个文件`); console.log(''); // 执行上传 let successCount = 0; let failCount = 0; const results = []; for (const skill of skills) { process.stdout.write(` 📦 ${skill.name} ... `); const skillResults = []; for (const file of skill.files) { try { const result = await uploadFile(file.localPath, file.cdnKey); skillResults.push({ ...result, fileName: file.fileName, status: 'ok' }); successCount++; } catch (err) { skillResults.push({ fileName: file.fileName, status: 'fail', error: err.message }); failCount++; } } const allOk = skillResults.every(r => r.status === 'ok'); console.log(allOk ? '✅' : '⚠️'); results.push({ name: skill.name, files: skillResults }); } // 输出结果 console.log(''); console.log('=================================================='); console.log(` 上传完成!成功: ${successCount} 失败: ${failCount}`); console.log('=================================================='); console.log(''); // 生成 URL 列表 console.log('📋 CDN 下载地址列表:'); console.log(''); const urlMap = {}; for (const skill of results) { const urls = {}; for (const file of skill.files) { if (file.status === 'ok') { urls[file.fileName] = file.url; console.log(` ${skill.name}/${file.fileName}`); console.log(` 👉 ${file.url}`); } else { console.log(` ${skill.name}/${file.fileName} ❌ ${file.error}`); } } if (Object.keys(urls).length > 0) { urlMap[skill.name] = urls; } } // 保存 URL 列表到文件 const outputPath = path.resolve(__dirname, '..', 'skills-cdn-urls.json'); fs.writeFileSync(outputPath, JSON.stringify(urlMap, null, 2), 'utf8'); console.log(''); console.log(`💾 URL 列表已保存到: ${outputPath}`); console.log(''); // 输出快速索引 URL console.log('📌 技能索引(可用于 OpenClaw 远程加载):'); console.log(` ${CDN_DOMAIN}/${CDN_PREFIX}/`); } main().catch(err => { console.error('❌ 上传出错:', err); process.exit(1); });