upload-skills.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/usr/bin/env node
  2. /**
  3. * OpenClaw Skills CDN 上传脚本
  4. *
  5. * 功能:将所有 skills 上传到七牛云 CDN,生成下载 URL
  6. *
  7. * 使用方法:
  8. * node scripts/upload-skills.js
  9. *
  10. * 前置条件:
  11. * 1. npm install qiniu
  12. * 2. 配置下方 QINIU_ACCESS_KEY 和 QINIU_SECRET_KEY
  13. *
  14. * 参考:hongben-server/modules/fmode-tikhub-server/upload.sh
  15. */
  16. const qiniu = require('qiniu');
  17. const fs = require('fs');
  18. const path = require('path');
  19. // ============================================
  20. // 七牛云配置(需要填入你的 AccessKey / SecretKey)
  21. // ============================================
  22. const QINIU_ACCESS_KEY = process.env.QINIU_AK || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE';
  23. const QINIU_SECRET_KEY = process.env.QINIU_SK || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX';
  24. const BUCKET = 'nova-repos';
  25. const CDN_DOMAIN = 'https://repos.fmode.cn';
  26. const CDN_PREFIX = 'x/openclaw-skills';
  27. // Skills 源目录(部署目录)
  28. const SKILLS_DIR = path.resolve(__dirname, '..', 'workshop');
  29. const DEPLOYED_SKILLS_DIR = path.join(process.env.USERPROFILE || process.env.HOME, '.openclaw', 'skills');
  30. // 要上传的技能目录列表(如果为空,则上传所有 skills)
  31. const SKILL_FILTER = []; // 例如 ['tiktok-video-search', 'jimeng-img-v4'] 指定只上传某些
  32. // ============================================
  33. // 上传逻辑
  34. // ============================================
  35. function getUploadToken(bucket, key) {
  36. const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);
  37. const putPolicy = new qiniu.rs.PutPolicy({
  38. scope: `${bucket}:${key}`, // 覆盖上传
  39. expires: 3600
  40. });
  41. return putPolicy.uploadToken(mac);
  42. }
  43. function uploadFile(localFile, cdnKey) {
  44. return new Promise((resolve, reject) => {
  45. const token = getUploadToken(BUCKET, cdnKey);
  46. const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({
  47. zone: qiniu.zone.Zone_z2 // 华南(nova-repos 所在区域)
  48. }));
  49. const putExtra = new qiniu.form_up.PutExtra();
  50. formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => {
  51. if (err) {
  52. reject(err);
  53. return;
  54. }
  55. if (info.statusCode === 200) {
  56. resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` });
  57. } else {
  58. reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`));
  59. }
  60. });
  61. });
  62. }
  63. async function collectSkills() {
  64. const skills = [];
  65. const dir = DEPLOYED_SKILLS_DIR;
  66. if (!fs.existsSync(dir)) {
  67. console.error(`Skills 目录不存在: ${dir}`);
  68. process.exit(1);
  69. }
  70. const entries = fs.readdirSync(dir).filter(d => {
  71. const fullPath = path.join(dir, d);
  72. if (!fs.statSync(fullPath).isDirectory()) return false;
  73. if (SKILL_FILTER.length > 0 && !SKILL_FILTER.includes(d)) return false;
  74. // 至少有 SKILL.md
  75. return fs.existsSync(path.join(fullPath, 'SKILL.md'));
  76. });
  77. for (const skillName of entries) {
  78. const skillDir = path.join(dir, skillName);
  79. const files = [];
  80. for (const fileName of ['SKILL.md', 'api-config.json']) {
  81. const filePath = path.join(skillDir, fileName);
  82. if (fs.existsSync(filePath)) {
  83. files.push({
  84. localPath: filePath,
  85. cdnKey: `${CDN_PREFIX}/${skillName}/${fileName}`,
  86. fileName
  87. });
  88. }
  89. }
  90. if (files.length > 0) {
  91. skills.push({ name: skillName, files });
  92. }
  93. }
  94. return skills;
  95. }
  96. async function main() {
  97. console.log('==================================================');
  98. console.log(' OpenClaw Skills CDN 上传脚本');
  99. console.log('==================================================');
  100. console.log('');
  101. // 检查凭证
  102. if (QINIU_ACCESS_KEY === 'YOUR_ACCESS_KEY_HERE' || QINIU_SECRET_KEY === 'YOUR_SECRET_KEY_HERE') {
  103. console.error('❌ 请先配置七牛云凭证!');
  104. console.error(' 方式1: 编辑脚本中的 QINIU_ACCESS_KEY / QINIU_SECRET_KEY');
  105. console.error(' 方式2: 设置环境变量 QINIU_AK / QINIU_SK');
  106. console.error('');
  107. console.error(' 示例: set QINIU_AK=xxx & set QINIU_SK=yyy & node scripts/upload-skills.js');
  108. process.exit(1);
  109. }
  110. // 收集技能
  111. const skills = await collectSkills();
  112. console.log(`📦 找到 ${skills.length} 个技能待上传`);
  113. console.log(`📂 源目录: ${DEPLOYED_SKILLS_DIR}`);
  114. console.log(`☁️ Bucket: ${BUCKET}`);
  115. console.log(`🔗 CDN前缀: ${CDN_PREFIX}`);
  116. console.log('');
  117. // 汇总文件
  118. let totalFiles = 0;
  119. skills.forEach(s => { totalFiles += s.files.length; });
  120. console.log(`📄 总计 ${totalFiles} 个文件`);
  121. console.log('');
  122. // 执行上传
  123. let successCount = 0;
  124. let failCount = 0;
  125. const results = [];
  126. for (const skill of skills) {
  127. process.stdout.write(` 📦 ${skill.name} ... `);
  128. const skillResults = [];
  129. for (const file of skill.files) {
  130. try {
  131. const result = await uploadFile(file.localPath, file.cdnKey);
  132. skillResults.push({ ...result, fileName: file.fileName, status: 'ok' });
  133. successCount++;
  134. } catch (err) {
  135. skillResults.push({ fileName: file.fileName, status: 'fail', error: err.message });
  136. failCount++;
  137. }
  138. }
  139. const allOk = skillResults.every(r => r.status === 'ok');
  140. console.log(allOk ? '✅' : '⚠️');
  141. results.push({ name: skill.name, files: skillResults });
  142. }
  143. // 输出结果
  144. console.log('');
  145. console.log('==================================================');
  146. console.log(` 上传完成!成功: ${successCount} 失败: ${failCount}`);
  147. console.log('==================================================');
  148. console.log('');
  149. // 生成 URL 列表
  150. console.log('📋 CDN 下载地址列表:');
  151. console.log('');
  152. const urlMap = {};
  153. for (const skill of results) {
  154. const urls = {};
  155. for (const file of skill.files) {
  156. if (file.status === 'ok') {
  157. urls[file.fileName] = file.url;
  158. console.log(` ${skill.name}/${file.fileName}`);
  159. console.log(` 👉 ${file.url}`);
  160. } else {
  161. console.log(` ${skill.name}/${file.fileName} ❌ ${file.error}`);
  162. }
  163. }
  164. if (Object.keys(urls).length > 0) {
  165. urlMap[skill.name] = urls;
  166. }
  167. }
  168. // 保存 URL 列表到文件
  169. const outputPath = path.resolve(__dirname, '..', 'skills-cdn-urls.json');
  170. fs.writeFileSync(outputPath, JSON.stringify(urlMap, null, 2), 'utf8');
  171. console.log('');
  172. console.log(`💾 URL 列表已保存到: ${outputPath}`);
  173. console.log('');
  174. // 输出快速索引 URL
  175. console.log('📌 技能索引(可用于 OpenClaw 远程加载):');
  176. console.log(` ${CDN_DOMAIN}/${CDN_PREFIX}/`);
  177. }
  178. main().catch(err => {
  179. console.error('❌ 上传出错:', err);
  180. process.exit(1);
  181. });