| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- #!/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);
- });
|