#!/usr/bin/env node /** * OpenClaw Skills 打包 + 上传脚本 * * 功能: * 1. 按套件分组,将 skills 打包成 zip(含 install.js + skills/ 目录) * 2. 上传 zip 到七牛云 CDN * 3. 输出下载链接 * * 使用:node scripts/deploy/package-and-upload.js * 前置:先运行 deploy-to-openclaw.ps1 确保 ~/.openclaw/skills 是最新的 */ const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); const qiniu = require('qiniu'); // ============================================ // 七牛云配置(同 upload-skills.js) // ============================================ 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/packages'; // ============================================ // 路径 // ============================================ const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills'); const INSTALL_JS = path.join(__dirname, '..', '..', 'dist', 'install.js'); const DIST_DIR = path.join(__dirname, '..', '..', 'dist'); const TEMP_BASE = path.join(os.tmpdir(), 'openclaw-pkg'); // ============================================ // 套件定义(技能名列表) // ============================================ const PACKAGES = { 'voc-core': { label: '🔍 VOC 核心套件', description: 'Amazon 品类洞察 + 评论分析 + 竞品分析 + 数据合成 + 社媒VOC + 工作坊', skills: [ // voc 'asin-reverse-keywords', 'asin-sales-volume', 'category-products', 'category-tree', 'keyword-product-ranking', 'keyword-search', 'keyword-search-trend', 'product-detail-query', 'product-monitor', 'product-reviews-query', 'product-search', 'similar-products', // review-analysis 'review-batch-collection', 'review-highlight-extraction', 'review-keyword-cloud', 'review-pain-point-extraction', 'review-sentiment-analysis', // competitor-analysis 'competitor-bsr-tracking', 'competitor-discovery', 'competitor-pricing-analysis', 'competitor-product-comparison', // synthesis 'brand-profile', 'category-landscape', 'html-report-generator', 'product-deep-analysis', 'user-persona', 'voc-proposal', // social-voc 'instagram-brand-voc', 'social-trend-analysis', 'tiktok-brand-voc', 'tiktok-category-voc', // workshop 'brand-context-builder' ] }, 'social-media': { label: '📱 社媒数据套件', description: 'TikTok / Instagram / 抖音数据采集', skills: [ // social-media 'instagram-search', 'instagram-user-info', 'instagram-user-posts', 'tiktok-hashtag-detail', 'tiktok-hashtag-videos', 'tiktok-user-posts', 'tiktok-user-profile', 'tiktok-user-search', 'tiktok-video-comments', 'tiktok-video-detail', 'tiktok-video-search', // douyin 'douyin-comment-replies', 'douyin-general-search', 'douyin-hashtag-search', 'douyin-user-profile', 'douyin-user-search', 'douyin-video-comments', 'douyin-video-detail' ] } }; // ============================================ // 七牛上传 // ============================================ function uploadFile(localFile, cdnKey) { return new Promise((resolve, reject) => { const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY); const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 }); const token = putPolicy.uploadToken(mac); const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 })); const putExtra = new qiniu.form_up.PutExtra(); formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => { if (err) return reject(err); if (info.statusCode === 200) { resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` }); } else { reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`)); } }); }); } // ============================================ // 打包逻辑 // ============================================ function buildPackage(pkgId, pkg) { const pkgDir = path.join(TEMP_BASE, pkgId); const skillsDir = path.join(pkgDir, 'skills'); // 清理 + 创建目录 if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true }); fs.mkdirSync(skillsDir, { recursive: true }); // 复制 install.js if (fs.existsSync(INSTALL_JS)) { fs.copyFileSync(INSTALL_JS, path.join(pkgDir, 'install.js')); } // 生成 README.txt const readme = [ `OpenClaw ${pkg.label}`, `${'='.repeat(50)}`, '', pkg.description, '', `包含 ${pkg.skills.length} 个技能`, '', '安装方法:', ' 1. 确保已安装 OpenClaw', ' 2. 解压本压缩包', ' 3. 在解压目录运行: node install.js', ' 4. 重启 OpenClaw gateway', '', '其他命令:', ' node install.js --list # 列出所有技能', ' node install.js --dry-run # 预览模式', ' node install.js --only a,b,c # 只安装指定技能', ' node install.js --skip a,b,c # 跳过指定技能', '', '技能列表:', ...pkg.skills.map(s => ` - ${s}`), '' ].join('\n'); fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8'); // 复制每个技能 let copied = 0; let missing = 0; for (const skillName of pkg.skills) { const srcDir = path.join(SKILLS_DIR, skillName); if (!fs.existsSync(srcDir)) { console.log(` ⚠️ 跳过 ${skillName}(未部署)`); missing++; continue; } const destDir = path.join(skillsDir, skillName); fs.mkdirSync(destDir, { recursive: true }); const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile()); for (const f of files) { fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f)); } copied++; } console.log(` ✅ 复制 ${copied} 个技能(跳过 ${missing} 个)`); // 压缩 const zipPath = path.join(DIST_DIR, `${pkgId}.zip`); if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); // 使用 PowerShell Compress-Archive const absZipPath = path.resolve(zipPath); const absPkgDir = path.resolve(pkgDir); execSync( `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`, { stdio: 'inherit' } ); const zipSize = fs.statSync(zipPath).size; console.log(` 📦 ${path.basename(zipPath)} (${(zipSize / 1024).toFixed(0)} KB)`); return zipPath; } // ============================================ // Main // ============================================ async function main() { console.log(''); console.log('=================================================='); console.log(' OpenClaw Skills 打包 + CDN 上传'); console.log('=================================================='); console.log(''); // 前置检查 if (!fs.existsSync(SKILLS_DIR)) { console.error(`❌ Skills 目录不存在: ${SKILLS_DIR}`); console.error(' 请先运行 deploy-to-openclaw.ps1'); process.exit(1); } if (!fs.existsSync(INSTALL_JS)) { console.error(`❌ install.js 不存在: ${INSTALL_JS}`); process.exit(1); } fs.mkdirSync(DIST_DIR, { recursive: true }); fs.mkdirSync(TEMP_BASE, { recursive: true }); // Step 1: 打包 console.log('── Step 1: 打包 ──'); const zipFiles = {}; for (const [pkgId, pkg] of Object.entries(PACKAGES)) { console.log(` ${pkg.label} (${pkg.skills.length} 个技能)`); zipFiles[pkgId] = buildPackage(pkgId, pkg); } console.log(''); // Step 2: 上传到七牛 console.log('── Step 2: 上传到七牛云 CDN ──'); const downloadUrls = {}; for (const [pkgId, zipPath] of Object.entries(zipFiles)) { const pkg = PACKAGES[pkgId]; const cdnKey = `${CDN_PREFIX}/${pkgId}.zip`; process.stdout.write(` ${pkg.label} ... `); try { const result = await uploadFile(zipPath, cdnKey); downloadUrls[pkgId] = result.url; console.log(`✅ ${result.url}`); } catch (err) { console.log(`❌ ${err.message}`); } } console.log(''); // 输出结果 console.log('=================================================='); console.log(' 📥 下载链接'); console.log('=================================================='); console.log(''); for (const [pkgId, url] of Object.entries(downloadUrls)) { const pkg = PACKAGES[pkgId]; console.log(` ${pkg.label}`); console.log(` 👉 ${url}`); console.log(''); } // 保存到文件 const outputPath = path.join(DIST_DIR, 'download-urls.json'); fs.writeFileSync(outputPath, JSON.stringify(downloadUrls, null, 2), 'utf8'); console.log(` 💾 链接已保存到: ${outputPath}`); console.log(''); // 清理临时目录 if (fs.existsSync(TEMP_BASE)) fs.rmSync(TEMP_BASE, { recursive: true, force: true }); } main().catch(err => { console.error('❌ 出错:', err); process.exit(1); });