package-and-upload.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. #!/usr/bin/env node
  2. /**
  3. * OpenClaw Skills 打包 + 上传脚本
  4. *
  5. * 功能:
  6. * 1. 按套件分组,将 skills 打包成 zip(含 install.js + skills/ 目录)
  7. * 2. 上传 zip 到七牛云 CDN
  8. * 3. 输出下载链接
  9. *
  10. * 使用:node scripts/deploy/package-and-upload.js
  11. * 前置:先运行 deploy-to-openclaw.ps1 确保 ~/.openclaw/skills 是最新的
  12. */
  13. const fs = require('fs');
  14. const path = require('path');
  15. const os = require('os');
  16. const { execSync } = require('child_process');
  17. const qiniu = require('qiniu');
  18. // ============================================
  19. // 七牛云配置(同 upload-skills.js)
  20. // ============================================
  21. const QINIU_ACCESS_KEY = process.env.QINIU_AK || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE';
  22. const QINIU_SECRET_KEY = process.env.QINIU_SK || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX';
  23. const BUCKET = 'nova-repos';
  24. const CDN_DOMAIN = 'https://repos.fmode.cn';
  25. const CDN_PREFIX = 'x/openclaw-skills/packages';
  26. // ============================================
  27. // 路径
  28. // ============================================
  29. const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills');
  30. const INSTALL_JS = path.join(__dirname, '..', '..', 'dist', 'install.js');
  31. const DIST_DIR = path.join(__dirname, '..', '..', 'dist');
  32. const TEMP_BASE = path.join(os.tmpdir(), 'openclaw-pkg');
  33. // ============================================
  34. // 套件定义(技能名列表)
  35. // ============================================
  36. const PACKAGES = {
  37. 'voc-core': {
  38. label: '🔍 VOC 核心套件',
  39. description: 'Amazon 品类洞察 + 评论分析 + 竞品分析 + 数据合成 + 社媒VOC + 工作坊',
  40. skills: [
  41. // voc
  42. 'asin-reverse-keywords', 'asin-sales-volume', 'category-products', 'category-tree',
  43. 'keyword-product-ranking', 'keyword-search', 'keyword-search-trend',
  44. 'product-detail-query', 'product-monitor', 'product-reviews-query', 'product-search', 'similar-products',
  45. // review-analysis
  46. 'review-batch-collection', 'review-highlight-extraction', 'review-keyword-cloud',
  47. 'review-pain-point-extraction', 'review-sentiment-analysis',
  48. // competitor-analysis
  49. 'competitor-bsr-tracking', 'competitor-discovery', 'competitor-pricing-analysis', 'competitor-product-comparison',
  50. // synthesis
  51. 'brand-profile', 'category-landscape', 'html-report-generator', 'product-deep-analysis', 'user-persona', 'voc-proposal',
  52. // social-voc
  53. 'instagram-brand-voc', 'social-trend-analysis', 'tiktok-brand-voc', 'tiktok-category-voc',
  54. // workshop
  55. 'brand-context-builder'
  56. ]
  57. },
  58. 'social-media': {
  59. label: '📱 社媒数据套件',
  60. description: 'TikTok / Instagram / 抖音数据采集',
  61. skills: [
  62. // social-media
  63. 'instagram-search', 'instagram-user-info', 'instagram-user-posts',
  64. 'tiktok-hashtag-detail', 'tiktok-hashtag-videos', 'tiktok-user-posts',
  65. 'tiktok-user-profile', 'tiktok-user-search', 'tiktok-video-comments',
  66. 'tiktok-video-detail', 'tiktok-video-search',
  67. // douyin
  68. 'douyin-comment-replies', 'douyin-general-search', 'douyin-hashtag-search',
  69. 'douyin-user-profile', 'douyin-user-search', 'douyin-video-comments', 'douyin-video-detail'
  70. ]
  71. }
  72. };
  73. // ============================================
  74. // 七牛上传
  75. // ============================================
  76. function uploadFile(localFile, cdnKey) {
  77. return new Promise((resolve, reject) => {
  78. const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);
  79. const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 });
  80. const token = putPolicy.uploadToken(mac);
  81. const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 }));
  82. const putExtra = new qiniu.form_up.PutExtra();
  83. formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => {
  84. if (err) return reject(err);
  85. if (info.statusCode === 200) {
  86. resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` });
  87. } else {
  88. reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`));
  89. }
  90. });
  91. });
  92. }
  93. // ============================================
  94. // 打包逻辑
  95. // ============================================
  96. function buildPackage(pkgId, pkg) {
  97. const pkgDir = path.join(TEMP_BASE, pkgId);
  98. const skillsDir = path.join(pkgDir, 'skills');
  99. // 清理 + 创建目录
  100. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  101. fs.mkdirSync(skillsDir, { recursive: true });
  102. // 复制 install.js
  103. if (fs.existsSync(INSTALL_JS)) {
  104. fs.copyFileSync(INSTALL_JS, path.join(pkgDir, 'install.js'));
  105. }
  106. // 生成 README.txt
  107. const readme = [
  108. `OpenClaw ${pkg.label}`,
  109. `${'='.repeat(50)}`,
  110. '',
  111. pkg.description,
  112. '',
  113. `包含 ${pkg.skills.length} 个技能`,
  114. '',
  115. '安装方法:',
  116. ' 1. 确保已安装 OpenClaw',
  117. ' 2. 解压本压缩包',
  118. ' 3. 在解压目录运行: node install.js',
  119. ' 4. 重启 OpenClaw gateway',
  120. '',
  121. '其他命令:',
  122. ' node install.js --list # 列出所有技能',
  123. ' node install.js --dry-run # 预览模式',
  124. ' node install.js --only a,b,c # 只安装指定技能',
  125. ' node install.js --skip a,b,c # 跳过指定技能',
  126. '',
  127. '技能列表:',
  128. ...pkg.skills.map(s => ` - ${s}`),
  129. ''
  130. ].join('\n');
  131. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  132. // 复制每个技能
  133. let copied = 0;
  134. let missing = 0;
  135. for (const skillName of pkg.skills) {
  136. const srcDir = path.join(SKILLS_DIR, skillName);
  137. if (!fs.existsSync(srcDir)) {
  138. console.log(` ⚠️ 跳过 ${skillName}(未部署)`);
  139. missing++;
  140. continue;
  141. }
  142. const destDir = path.join(skillsDir, skillName);
  143. fs.mkdirSync(destDir, { recursive: true });
  144. const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
  145. for (const f of files) {
  146. fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
  147. }
  148. copied++;
  149. }
  150. console.log(` ✅ 复制 ${copied} 个技能(跳过 ${missing} 个)`);
  151. // 压缩
  152. const zipPath = path.join(DIST_DIR, `${pkgId}.zip`);
  153. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  154. // 使用 PowerShell Compress-Archive
  155. const absZipPath = path.resolve(zipPath);
  156. const absPkgDir = path.resolve(pkgDir);
  157. execSync(
  158. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  159. { stdio: 'inherit' }
  160. );
  161. const zipSize = fs.statSync(zipPath).size;
  162. console.log(` 📦 ${path.basename(zipPath)} (${(zipSize / 1024).toFixed(0)} KB)`);
  163. return zipPath;
  164. }
  165. // ============================================
  166. // Main
  167. // ============================================
  168. async function main() {
  169. console.log('');
  170. console.log('==================================================');
  171. console.log(' OpenClaw Skills 打包 + CDN 上传');
  172. console.log('==================================================');
  173. console.log('');
  174. // 前置检查
  175. if (!fs.existsSync(SKILLS_DIR)) {
  176. console.error(`❌ Skills 目录不存在: ${SKILLS_DIR}`);
  177. console.error(' 请先运行 deploy-to-openclaw.ps1');
  178. process.exit(1);
  179. }
  180. if (!fs.existsSync(INSTALL_JS)) {
  181. console.error(`❌ install.js 不存在: ${INSTALL_JS}`);
  182. process.exit(1);
  183. }
  184. fs.mkdirSync(DIST_DIR, { recursive: true });
  185. fs.mkdirSync(TEMP_BASE, { recursive: true });
  186. // Step 1: 打包
  187. console.log('── Step 1: 打包 ──');
  188. const zipFiles = {};
  189. for (const [pkgId, pkg] of Object.entries(PACKAGES)) {
  190. console.log(` ${pkg.label} (${pkg.skills.length} 个技能)`);
  191. zipFiles[pkgId] = buildPackage(pkgId, pkg);
  192. }
  193. console.log('');
  194. // Step 2: 上传到七牛
  195. console.log('── Step 2: 上传到七牛云 CDN ──');
  196. const downloadUrls = {};
  197. for (const [pkgId, zipPath] of Object.entries(zipFiles)) {
  198. const pkg = PACKAGES[pkgId];
  199. const cdnKey = `${CDN_PREFIX}/${pkgId}.zip`;
  200. process.stdout.write(` ${pkg.label} ... `);
  201. try {
  202. const result = await uploadFile(zipPath, cdnKey);
  203. downloadUrls[pkgId] = result.url;
  204. console.log(`✅ ${result.url}`);
  205. } catch (err) {
  206. console.log(`❌ ${err.message}`);
  207. }
  208. }
  209. console.log('');
  210. // 输出结果
  211. console.log('==================================================');
  212. console.log(' 📥 下载链接');
  213. console.log('==================================================');
  214. console.log('');
  215. for (const [pkgId, url] of Object.entries(downloadUrls)) {
  216. const pkg = PACKAGES[pkgId];
  217. console.log(` ${pkg.label}`);
  218. console.log(` 👉 ${url}`);
  219. console.log('');
  220. }
  221. // 保存到文件
  222. const outputPath = path.join(DIST_DIR, 'download-urls.json');
  223. fs.writeFileSync(outputPath, JSON.stringify(downloadUrls, null, 2), 'utf8');
  224. console.log(` 💾 链接已保存到: ${outputPath}`);
  225. console.log('');
  226. // 清理临时目录
  227. if (fs.existsSync(TEMP_BASE)) fs.rmSync(TEMP_BASE, { recursive: true, force: true });
  228. }
  229. main().catch(err => {
  230. console.error('❌ 出错:', err);
  231. process.exit(1);
  232. });