package-and-upload.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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 SET_VOC_TOKEN_JS = path.join(__dirname, '..', 'tools', 'set-voc-token.js');
  32. const VOC_TOKEN_PREFLIGHT_JS = path.join(__dirname, '..', 'tools', 'voc-token-preflight.js');
  33. const INSTALL_WORKSHOP_JS = path.join(__dirname, 'install-workshop.js');
  34. const INSTALL_ALL_JS = path.join(__dirname, 'install-all.js');
  35. const CREDENTIALS_TEMPLATE = path.join(__dirname, '..', '..', '__config', 'voc-credentials.template.json');
  36. const DIST_DIR = path.join(__dirname, '..', '..', 'dist');
  37. const TEMP_BASE = path.join(os.tmpdir(), 'openclaw-pkg');
  38. const WORKSHOP_DIR = path.join(os.homedir(), '.openclaw', 'workspace');
  39. const MEMORY_TEMPLATES_DIR = path.join(WORKSHOP_DIR, 'memory');
  40. // OpenClaw 系统文件(不能打包,否则覆盖用户配置)
  41. const SYSTEM_MD_FILES = new Set([
  42. 'AGENTS.md', 'BOOTSTRAP.md', 'HEARTBEAT.md', 'IDENTITY.md',
  43. 'SOUL.md', 'TOOLS.md', 'USER.md'
  44. ]);
  45. // Workshop playbook 白名单(只打包这些 .md)
  46. const WORKSHOP_MD_WHITELIST = new Set([
  47. 'workshop-voc-playbook.md',
  48. 'workshop-global-rules.md',
  49. 'workshop-session-0-intake.md',
  50. 'workshop-session-1-category.md',
  51. 'workshop-session-2-brand.md',
  52. 'workshop-session-3-voc.md',
  53. 'workshop-session-4-deep.md',
  54. 'workshop-session-5-report.md',
  55. 'product-analysis-playbook.md',
  56. 'synthesis-prompts.md',
  57. 'voc-report-schema-spec.md'
  58. ]);
  59. // Memory template 白名单(排除运行时数据)
  60. const MEMORY_TEMPLATE_WHITELIST = new Set([
  61. 'stage-1-output.json',
  62. 'stage-2-output.json',
  63. 'stage-2-competitor-output.json',
  64. 'stage-3-output.json',
  65. 'stage-4-output.json',
  66. 'stage-5-output.json',
  67. 'workshop-progress.json',
  68. 'brand-context.json',
  69. 'calibration-notes.json',
  70. 'analysis-request.json'
  71. ]);
  72. // 测试技能黑名单
  73. const SKILL_BLACKLIST = new Set(['test-billing', 'test-skill']);
  74. // ============================================
  75. // 套件定义(技能名列表)
  76. // ============================================
  77. const PACKAGES = {
  78. 'voc-core': {
  79. label: '🔍 VOC 核心套件',
  80. description: 'Amazon 品类洞察 + 评论分析 + 竞品分析 + 数据合成 + 社媒VOC + 工作坊',
  81. skills: [
  82. // voc
  83. 'asin-reverse-keywords', 'asin-sales-volume', 'category-products', 'category-tree',
  84. 'keyword-product-ranking', 'keyword-search', 'keyword-search-trend',
  85. 'product-detail-query', 'product-monitor', 'product-reviews-query', 'product-search', 'similar-products',
  86. // review-analysis
  87. 'review-batch-collection', 'review-highlight-extraction', 'review-keyword-cloud',
  88. 'review-pain-point-extraction', 'review-sentiment-analysis',
  89. // competitor-analysis
  90. 'competitor-bsr-tracking', 'competitor-discovery', 'competitor-pricing-analysis', 'competitor-product-comparison',
  91. // synthesis
  92. 'brand-profile', 'category-landscape', 'html-report-generator', 'product-deep-analysis', 'user-persona', 'voc-proposal',
  93. // social-voc
  94. 'instagram-brand-voc', 'social-trend-analysis', 'tiktok-brand-voc', 'tiktok-category-voc',
  95. // workshop
  96. 'brand-context-builder', 'voc-token-preflight'
  97. ]
  98. },
  99. 'social-media': {
  100. label: '📱 社媒数据套件',
  101. description: 'TikTok / Instagram / 抖音数据采集',
  102. skills: [
  103. // social-media
  104. 'instagram-search', 'instagram-user-info', 'instagram-user-posts',
  105. 'tiktok-hashtag-detail', 'tiktok-hashtag-videos', 'tiktok-user-posts',
  106. 'tiktok-user-profile', 'tiktok-user-search', 'tiktok-video-comments',
  107. 'tiktok-video-detail', 'tiktok-video-search',
  108. // douyin
  109. 'douyin-comment-replies', 'douyin-general-search', 'douyin-hashtag-search',
  110. 'douyin-user-profile', 'douyin-user-search', 'douyin-video-comments', 'douyin-video-detail'
  111. ]
  112. }
  113. };
  114. // ============================================
  115. // 七牛上传
  116. // ============================================
  117. function uploadFile(localFile, cdnKey) {
  118. return new Promise((resolve, reject) => {
  119. const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);
  120. const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 });
  121. const token = putPolicy.uploadToken(mac);
  122. const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 }));
  123. const putExtra = new qiniu.form_up.PutExtra();
  124. formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => {
  125. if (err) return reject(err);
  126. if (info.statusCode === 200) {
  127. resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` });
  128. } else {
  129. reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`));
  130. }
  131. });
  132. });
  133. }
  134. // ============================================
  135. // 打包逻辑
  136. // ============================================
  137. function buildPackage(pkgId, pkg) {
  138. const pkgDir = path.join(TEMP_BASE, pkgId);
  139. const skillsDir = path.join(pkgDir, 'skills');
  140. // 清理 + 创建目录
  141. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  142. fs.mkdirSync(skillsDir, { recursive: true });
  143. // 复制 install.js
  144. if (fs.existsSync(INSTALL_JS)) {
  145. fs.copyFileSync(INSTALL_JS, path.join(pkgDir, 'install.js'));
  146. }
  147. // 复制 set-voc-token.js(Token 设置工具)
  148. if (fs.existsSync(SET_VOC_TOKEN_JS)) {
  149. fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
  150. }
  151. // 复制 voc-token-preflight.js(Token 预飞检测工具)
  152. if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
  153. fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
  154. }
  155. // 生成 README.txt
  156. const readme = [
  157. `OpenClaw ${pkg.label}`,
  158. `${'='.repeat(50)}`,
  159. '',
  160. pkg.description,
  161. '',
  162. `包含 ${pkg.skills.length} 个技能`,
  163. '',
  164. '安装方法:',
  165. ' 1. 确保已安装 OpenClaw',
  166. ' 2. 解压本压缩包',
  167. ' 3. 在解压目录运行: node install.js',
  168. ' 4. 设置 Token: node set-voc-token.js <your-session-token>',
  169. ' 5. 重启 OpenClaw gateway',
  170. '',
  171. '其他命令:',
  172. ' node install.js --list # 列出所有技能',
  173. ' node install.js --dry-run # 预览模式',
  174. ' node install.js --only a,b,c # 只安装指定技能',
  175. ' node install.js --skip a,b,c # 跳过指定技能',
  176. ' node set-voc-token.js <token> # 设置/更新 VOC Token',
  177. ' node voc-token-preflight.js # 检测 Token 有效性和余额',
  178. '',
  179. '技能列表:',
  180. ...pkg.skills.map(s => ` - ${s}`),
  181. ''
  182. ].join('\n');
  183. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  184. // 复制每个技能
  185. let copied = 0;
  186. let missing = 0;
  187. for (const skillName of pkg.skills) {
  188. const srcDir = path.join(SKILLS_DIR, skillName);
  189. if (!fs.existsSync(srcDir)) {
  190. console.log(` ⚠️ 跳过 ${skillName}(未部署)`);
  191. missing++;
  192. continue;
  193. }
  194. const destDir = path.join(skillsDir, skillName);
  195. fs.mkdirSync(destDir, { recursive: true });
  196. const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
  197. for (const f of files) {
  198. fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
  199. }
  200. copied++;
  201. }
  202. console.log(` ✅ 复制 ${copied} 个技能(跳过 ${missing} 个)`);
  203. // 压缩
  204. const zipPath = path.join(DIST_DIR, `${pkgId}.zip`);
  205. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  206. // 使用 PowerShell Compress-Archive
  207. const absZipPath = path.resolve(zipPath);
  208. const absPkgDir = path.resolve(pkgDir);
  209. execSync(
  210. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  211. { stdio: 'inherit' }
  212. );
  213. const zipSize = fs.statSync(zipPath).size;
  214. console.log(` 📦 ${path.basename(zipPath)} (${(zipSize / 1024).toFixed(0)} KB)`);
  215. return zipPath;
  216. }
  217. // ============================================
  218. // Workshop 打包逻辑
  219. // ============================================
  220. function buildWorkshopPackage() {
  221. const pkgDir = path.join(TEMP_BASE, 'workshop');
  222. const memoryDir = path.join(pkgDir, 'memory-templates');
  223. const skillsDir = path.join(pkgDir, 'skills');
  224. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  225. fs.mkdirSync(memoryDir, { recursive: true });
  226. fs.mkdirSync(skillsDir, { recursive: true });
  227. // 复制安装脚本
  228. if (fs.existsSync(INSTALL_WORKSHOP_JS)) {
  229. fs.copyFileSync(INSTALL_WORKSHOP_JS, path.join(pkgDir, 'install-workshop.js'));
  230. }
  231. // 复制 Token 工具(workshop 自足所需)
  232. if (fs.existsSync(SET_VOC_TOKEN_JS)) {
  233. fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
  234. }
  235. if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
  236. fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
  237. }
  238. // 复制凭证模板
  239. if (fs.existsSync(CREDENTIALS_TEMPLATE)) {
  240. fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json'));
  241. }
  242. let copied = 0;
  243. // 复制 workshop 文件(仅白名单 playbooks)
  244. if (fs.existsSync(WORKSHOP_DIR)) {
  245. const mdFiles = fs.readdirSync(WORKSHOP_DIR).filter(f =>
  246. f.endsWith('.md') && WORKSHOP_MD_WHITELIST.has(f) && fs.statSync(path.join(WORKSHOP_DIR, f)).isFile()
  247. );
  248. for (const f of mdFiles) {
  249. fs.copyFileSync(path.join(WORKSHOP_DIR, f), path.join(pkgDir, f));
  250. copied++;
  251. }
  252. }
  253. // 复制 memory templates(仅白名单模板)
  254. if (fs.existsSync(MEMORY_TEMPLATES_DIR)) {
  255. const jsonFiles = fs.readdirSync(MEMORY_TEMPLATES_DIR).filter(f =>
  256. f.endsWith('.json') && MEMORY_TEMPLATE_WHITELIST.has(f) && fs.statSync(path.join(MEMORY_TEMPLATES_DIR, f)).isFile()
  257. );
  258. for (const f of jsonFiles) {
  259. fs.copyFileSync(path.join(MEMORY_TEMPLATES_DIR, f), path.join(memoryDir, f));
  260. copied++;
  261. }
  262. }
  263. // 复制 workshop 所需的全部 skills(复用 voc-core 列表,workshop 主线+侧线都依赖它们)
  264. const workshopSkills = PACKAGES['voc-core'].skills;
  265. let skillsCopied = 0;
  266. let skillsMissing = 0;
  267. for (const skillName of workshopSkills) {
  268. const srcDir = path.join(SKILLS_DIR, skillName);
  269. if (!fs.existsSync(srcDir)) {
  270. skillsMissing++;
  271. continue;
  272. }
  273. const destDir = path.join(skillsDir, skillName);
  274. fs.mkdirSync(destDir, { recursive: true });
  275. const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
  276. for (const f of files) {
  277. fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
  278. }
  279. skillsCopied++;
  280. }
  281. // 生成 README.txt
  282. const readme = [
  283. 'OpenClaw 🦐 VOC虾工作坊 v4.2(自足版)',
  284. '==================================================',
  285. '',
  286. '一个压缩包搞定所有!包含:',
  287. ` 🔧 ${skillsCopied} 个 workshop 必需的 skills(品类/评论/竞品/合成/社媒VOC/工作坊)`,
  288. ' 📘 11 份 playbook(入口+全局规则+6个Session+配套文档)',
  289. ' 📄 9 份 memory templates(记忆模板)',
  290. ' 🔑 2 个 Token 工具(set-voc-token.js + voc-token-preflight.js)',
  291. ' 📝 1 份凭证模板(voc-credentials.template.json)',
  292. '',
  293. '架构:6个Session + 17个Side Quest,主线感知 + 支线深耕',
  294. '',
  295. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  296. '📦 一键安装:',
  297. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  298. ' 1. 解压本压缩包',
  299. ' 2. 运行: node install-workshop.js',
  300. ' (自动安装 skills + playbooks + memory templates + tools + 凭证模板)',
  301. ' 3. 设置 Token(首次使用):',
  302. ' a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF',
  303. ' b. 登录并充值(最低 ¥0.01 体验 1 次)',
  304. ' c. 复制 session token(格式 r:xxxx...)',
  305. ' d. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>',
  306. ' 4. 重启 OpenClaw gateway',
  307. ' 5. 对话框说"开始 VOC 工作坊",Agent 会自动进入 Session 0',
  308. '',
  309. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  310. '🔍 验证安装:',
  311. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  312. ' node ~/.openclaw/tools/voc-token-preflight.js',
  313. ' (期望看到: status=valid, 余额 >= 1)',
  314. '',
  315. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  316. '🛠 其他命令:',
  317. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  318. ' node install-workshop.js --dry-run # 预览将要安装的文件',
  319. ' node install-workshop.js --skills-only # 只装 skills',
  320. ' node install-workshop.js --playbook-only # 只装 playbooks (旧行为)',
  321. '',
  322. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  323. '📂 文件结构:',
  324. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  325. ' skills/ ← workshop 用到的 skills',
  326. ' voc-token-preflight/ ← Session 0/1 的 Token 预飞闸门',
  327. ' brand-context-builder/ ← Session 0 品牌建档',
  328. ' category-landscape/ ← Session 1 品类全景',
  329. ' keyword-search/, category-tree/, ... (其余 Amazon/VOC/合成 skills)',
  330. ' workshop-voc-playbook.md ← 入口路由',
  331. ' workshop-global-rules.md ← 全局规则 + Token 预飞协议',
  332. ' workshop-session-0-intake.md ← Session 0: 品牌建档 + Token 预飞',
  333. ' workshop-session-1-category.md ← Session 1: 品类 + 4 Side Quests',
  334. ' workshop-session-2-brand.md ← Session 2: 品牌 + 3 Side Quests',
  335. ' workshop-session-3-voc.md ← Session 3: VOC + 3 Side Quests',
  336. ' workshop-session-4-deep.md ← Session 4: 深入 + 5 Side Quests',
  337. ' workshop-session-5-report.md ← Session 5: 报告 + 2 Side Quests',
  338. ' memory-templates/*.json ← 记忆模板(9个)',
  339. ' set-voc-token.js ← Token 写入工具',
  340. ' voc-token-preflight.js ← Token 预飞检测工具',
  341. ' voc-credentials.template.json ← 凭证模板',
  342. ''
  343. ].join('\n');
  344. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  345. console.log(` ✅ 复制 ${copied} 个 playbook/template + ${skillsCopied} 个 skills` +
  346. (skillsMissing > 0 ? ` (缺失 ${skillsMissing})` : ''));
  347. // 压缩
  348. const zipPath = path.join(DIST_DIR, 'workshop.zip');
  349. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  350. const absZipPath = path.resolve(zipPath);
  351. const absPkgDir = path.resolve(pkgDir);
  352. execSync(
  353. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  354. { stdio: 'inherit' }
  355. );
  356. const zipSize = fs.statSync(zipPath).size;
  357. console.log(` 📦 workshop.zip (${(zipSize / 1024).toFixed(0)} KB)`);
  358. return zipPath;
  359. }
  360. // ============================================
  361. // 全量包打包逻辑
  362. // ============================================
  363. function buildAllPackage() {
  364. const pkgDir = path.join(TEMP_BASE, 'openclaw-all');
  365. const skillsDir = path.join(pkgDir, 'skills');
  366. const workshopDir = path.join(pkgDir, 'workshop');
  367. const memoryDir = path.join(workshopDir, 'memory-templates');
  368. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  369. fs.mkdirSync(memoryDir, { recursive: true });
  370. // 复制 install-all.js
  371. if (fs.existsSync(INSTALL_ALL_JS)) {
  372. fs.copyFileSync(INSTALL_ALL_JS, path.join(pkgDir, 'install-all.js'));
  373. }
  374. // 复制 set-voc-token.js
  375. if (fs.existsSync(SET_VOC_TOKEN_JS)) {
  376. fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
  377. }
  378. // 复制 voc-token-preflight.js
  379. if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
  380. fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
  381. }
  382. // 复制凭证模板
  383. if (fs.existsSync(CREDENTIALS_TEMPLATE)) {
  384. fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json'));
  385. }
  386. let skillCount = 0;
  387. let workshopCount = 0;
  388. // 复制发布套件中的技能(排除测试技能和未发布的即梦/视频创作技能)
  389. const RELEASED_SKILLS = new Set([
  390. ...PACKAGES['voc-core'].skills,
  391. ...PACKAGES['social-media'].skills
  392. ]);
  393. if (fs.existsSync(SKILLS_DIR)) {
  394. const allSkills = fs.readdirSync(SKILLS_DIR).filter(d => {
  395. const p = path.join(SKILLS_DIR, d);
  396. return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'))
  397. && !SKILL_BLACKLIST.has(d) && RELEASED_SKILLS.has(d);
  398. });
  399. for (const skill of allSkills) {
  400. const srcDir = path.join(SKILLS_DIR, skill);
  401. const destDir = path.join(skillsDir, skill);
  402. fs.mkdirSync(destDir, { recursive: true });
  403. const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
  404. for (const f of files) {
  405. fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
  406. }
  407. skillCount++;
  408. }
  409. }
  410. console.log(` ✅ 复制 ${skillCount} 个技能`);
  411. // 复制 workshop 文件(仅白名单)
  412. if (fs.existsSync(WORKSHOP_DIR)) {
  413. const mdFiles = fs.readdirSync(WORKSHOP_DIR).filter(f =>
  414. f.endsWith('.md') && WORKSHOP_MD_WHITELIST.has(f) && fs.statSync(path.join(WORKSHOP_DIR, f)).isFile()
  415. );
  416. for (const f of mdFiles) {
  417. fs.copyFileSync(path.join(WORKSHOP_DIR, f), path.join(workshopDir, f));
  418. workshopCount++;
  419. }
  420. }
  421. if (fs.existsSync(MEMORY_TEMPLATES_DIR)) {
  422. const jsonFiles = fs.readdirSync(MEMORY_TEMPLATES_DIR).filter(f =>
  423. f.endsWith('.json') && MEMORY_TEMPLATE_WHITELIST.has(f) && fs.statSync(path.join(MEMORY_TEMPLATES_DIR, f)).isFile()
  424. );
  425. for (const f of jsonFiles) {
  426. fs.copyFileSync(path.join(MEMORY_TEMPLATES_DIR, f), path.join(memoryDir, f));
  427. workshopCount++;
  428. }
  429. }
  430. console.log(` ✅ 复制 ${workshopCount} 个 workshop 文件`);
  431. // README.txt
  432. const readme = [
  433. 'OpenClaw 全量安装包 v4.1',
  434. '==================================================',
  435. '',
  436. `包含 ${skillCount} 个技能 + VOC虾工作坊 (${workshopCount}个文件) + Token 工具`,
  437. '',
  438. '安装方法:',
  439. ' 1. 解压本压缩包',
  440. ' 2. 在解压目录运行: node install-all.js',
  441. ' 3. 设置 Token:',
  442. ' a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF',
  443. ' b. 登录并充值',
  444. ' c. 获取 session token (r:xxxx...)',
  445. ' d. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>',
  446. ' 4. 重启 OpenClaw gateway',
  447. '',
  448. '其他命令:',
  449. ' node install-all.js --list # 列出包含内容',
  450. ' node install-all.js --dry-run # 预览模式',
  451. ' node install-all.js --skills-only # 只装 Skills',
  452. ' node install-all.js --workshop-only # 只装 Workshop',
  453. ' node set-voc-token.js <token> # 设置/更新 Token',
  454. ' node voc-token-preflight.js # 检测 Token 有效性和余额(Workshop 前置检查)',
  455. ''
  456. ].join('\n');
  457. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  458. // 压缩
  459. const zipPath = path.join(DIST_DIR, 'openclaw-all.zip');
  460. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  461. const absZipPath = path.resolve(zipPath);
  462. const absPkgDir = path.resolve(pkgDir);
  463. execSync(
  464. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  465. { stdio: 'inherit' }
  466. );
  467. const zipSize = fs.statSync(zipPath).size;
  468. console.log(` 📦 openclaw-all.zip (${(zipSize / 1024).toFixed(0)} KB)`);
  469. return zipPath;
  470. }
  471. // ============================================
  472. // Main
  473. // ============================================
  474. async function main() {
  475. console.log('');
  476. console.log('==================================================');
  477. console.log(' OpenClaw Skills + Workshop 打包 + CDN 上传 v4.1');
  478. console.log('==================================================');
  479. console.log('');
  480. // 前置检查
  481. if (!fs.existsSync(SKILLS_DIR)) {
  482. console.error(`❌ Skills 目录不存在: ${SKILLS_DIR}`);
  483. console.error(' 请先运行 deploy-to-openclaw.ps1');
  484. process.exit(1);
  485. }
  486. if (!fs.existsSync(INSTALL_JS)) {
  487. console.error(`❌ install.js 不存在: ${INSTALL_JS}`);
  488. process.exit(1);
  489. }
  490. fs.mkdirSync(DIST_DIR, { recursive: true });
  491. fs.mkdirSync(TEMP_BASE, { recursive: true });
  492. // Step 1: 打包 Skills
  493. console.log('── Step 1: 打包 Skills ──');
  494. const zipFiles = {};
  495. for (const [pkgId, pkg] of Object.entries(PACKAGES)) {
  496. console.log(` ${pkg.label} (${pkg.skills.length} 个技能)`);
  497. zipFiles[pkgId] = buildPackage(pkgId, pkg);
  498. }
  499. console.log('');
  500. // Step 1b: 打包 Workshop
  501. console.log('── Step 1b: 打包 Workshop ──');
  502. if (fs.existsSync(WORKSHOP_DIR)) {
  503. console.log(' 🦐 VOC虾工作坊');
  504. zipFiles['workshop'] = buildWorkshopPackage();
  505. } else {
  506. console.log(' ⚠️ Workshop 目录不存在,跳过');
  507. }
  508. console.log('');
  509. // Step 1c: 打包全量包
  510. console.log('── Step 1c: 打包全量包 ──');
  511. console.log(' 📦 openclaw-all (Skills + Workshop + Token工具)');
  512. zipFiles['openclaw-all'] = buildAllPackage();
  513. console.log('');
  514. // Step 2: 上传到七牛
  515. console.log('── Step 2: 上传到七牛云 CDN ──');
  516. const downloadUrls = {};
  517. for (const [pkgId, zipPath] of Object.entries(zipFiles)) {
  518. const pkg = PACKAGES[pkgId];
  519. const cdnKey = `${CDN_PREFIX}/${pkgId}.zip`;
  520. const label = pkgId === 'workshop' ? '🦐 VOC虾工作坊' : pkgId === 'openclaw-all' ? '📦 全量包' : pkg.label;
  521. process.stdout.write(` ${label} ... `);
  522. try {
  523. const result = await uploadFile(zipPath, cdnKey);
  524. downloadUrls[pkgId] = result.url;
  525. console.log(`✅ ${result.url}`);
  526. } catch (err) {
  527. console.log(`❌ ${err.message}`);
  528. }
  529. }
  530. console.log('');
  531. // 输出结果
  532. console.log('==================================================');
  533. console.log(' 📥 下载链接');
  534. console.log('==================================================');
  535. console.log('');
  536. for (const [pkgId, url] of Object.entries(downloadUrls)) {
  537. if (pkgId === 'workshop') {
  538. console.log(' 🦐 VOC虾工作坊');
  539. } else if (pkgId === 'openclaw-all') {
  540. console.log(' 📦 全量包');
  541. } else {
  542. const pkg = PACKAGES[pkgId];
  543. if (pkg) console.log(` ${pkg.label}`);
  544. }
  545. console.log(` 👉 ${url}`);
  546. console.log('');
  547. }
  548. // 保存到文件
  549. const outputPath = path.join(DIST_DIR, 'download-urls.json');
  550. fs.writeFileSync(outputPath, JSON.stringify(downloadUrls, null, 2), 'utf8');
  551. console.log(` 💾 链接已保存到: ${outputPath}`);
  552. console.log('');
  553. // 清理临时目录
  554. if (fs.existsSync(TEMP_BASE)) fs.rmSync(TEMP_BASE, { recursive: true, force: true });
  555. }
  556. main().catch(err => {
  557. console.error('❌ 出错:', err);
  558. process.exit(1);
  559. });