package-and-upload.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  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 PROJECT_ROOT = path.join(__dirname, '..', '..');
  30. const OPENCLAW_SKILLS_ROOT = path.join(PROJECT_ROOT, 'openclaw-skills');
  31. const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills');
  32. const INSTALL_JS = path.join(__dirname, '..', '..', 'dist', 'install.js');
  33. const SET_VOC_TOKEN_JS = path.join(__dirname, '..', 'tools', 'set-voc-token.js');
  34. const VOC_TOKEN_PREFLIGHT_JS = path.join(__dirname, '..', 'tools', 'voc-token-preflight.js');
  35. const INSTALL_WORKSHOP_JS = path.join(__dirname, 'install-workshop.js');
  36. const INSTALL_ALL_JS = path.join(__dirname, 'install-all.js');
  37. const CREDENTIALS_TEMPLATE = path.join(__dirname, '..', '..', '__config', 'voc-credentials.template.json');
  38. const DIST_DIR = path.join(__dirname, '..', '..', 'dist');
  39. const TEMP_BASE = path.join(os.tmpdir(), 'openclaw-pkg');
  40. const WORKSHOP_DIR = path.join(os.homedir(), '.openclaw', 'workspace');
  41. const MEMORY_TEMPLATES_DIR = path.join(WORKSHOP_DIR, 'memory');
  42. const TOOL_PACKAGE_FILES = [
  43. { src: path.join(__dirname, '..', 'tools', 'openclaw-tool-runner.js'), dest: 'openclaw-tool-runner.js' },
  44. { src: path.join(__dirname, '..', 'tools', 'set-voc-token.js'), dest: 'set-voc-token.js' },
  45. { src: path.join(__dirname, '..', 'tools', 'voc-token-preflight.js'), dest: 'voc-token-preflight.js' },
  46. { src: path.join(__dirname, '..', 'tools', 'voc-single-platform-report.js'), dest: 'voc-single-platform-report.js' },
  47. { src: path.join(__dirname, '..', 'tools', 'voc-data-normalizer.js'), dest: 'voc-data-normalizer.js' },
  48. { src: path.join(__dirname, '..', 'tools', 'platform-mini-report-generator.js'), dest: 'platform-mini-report-generator.js' },
  49. { src: path.join(__dirname, '..', 'tools', 'chapter-insight-writer.js'), dest: 'chapter-insight-writer.js' },
  50. { src: path.join(__dirname, '..', 'tools', 'voc-report-auditor.js'), dest: 'voc-report-auditor.js' },
  51. { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-profile-memory.js'), dest: 'douyin-speaking-profile-memory.js' },
  52. { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-daily-runner.js'), dest: 'douyin-speaking-daily-runner.js' },
  53. { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-daily-report.js'), dest: 'douyin-speaking-daily-report.js' },
  54. { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-daily-p1-smoke.js'), dest: 'douyin-speaking-daily-p1-smoke.js' },
  55. { src: path.join(__dirname, '..', 'tools', 'douyin-viral-script-analyzer.js'), dest: 'douyin-viral-script-analyzer.js' },
  56. { src: path.join(__dirname, '..', 'tools', 'douyin-video-transcriber.js'), dest: 'douyin-video-transcriber.js' },
  57. { src: path.join(OPENCLAW_SKILLS_ROOT, 'voc-report-factory', 'html-v2', 'gen-report-template.js'), dest: path.join('voc-report-factory', 'html-v2', 'gen-report-template.js') },
  58. { src: path.join(OPENCLAW_SKILLS_ROOT, 'voc-report-factory', 'html-v2', 'report-renderer-template.js'), dest: path.join('voc-report-factory', 'html-v2', 'report-renderer-template.js') },
  59. { src: path.join(__dirname, '..', '..', 'demo', 'xiaohongshu', 'raw-notes.json'), dest: path.join('course-samples', 'xiaohongshu', 'raw-notes.json') },
  60. { src: path.join(__dirname, '..', '..', 'demo', 'xiaohongshu', 'raw-comments.json'), dest: path.join('course-samples', 'xiaohongshu', 'raw-comments.json') }
  61. ];
  62. // OpenClaw 系统文件(不能打包,否则覆盖用户配置)
  63. const SYSTEM_MD_FILES = new Set([
  64. 'AGENTS.md', 'BOOTSTRAP.md', 'HEARTBEAT.md', 'IDENTITY.md',
  65. 'SOUL.md', 'TOOLS.md', 'USER.md'
  66. ]);
  67. // Workshop playbook 白名单(只打包这些 .md)
  68. const WORKSHOP_MD_WHITELIST = new Set([
  69. 'workshop-voc-playbook.md',
  70. 'workshop-global-rules.md',
  71. 'workshop-session-0-intake.md',
  72. 'workshop-session-1-category.md',
  73. 'workshop-session-2-brand.md',
  74. 'workshop-session-3-voc.md',
  75. 'workshop-session-4-deep.md',
  76. 'workshop-session-5-report.md',
  77. 'product-analysis-playbook.md',
  78. 'synthesis-prompts.md',
  79. 'voc-report-schema-spec.md'
  80. ]);
  81. // Memory template 白名单(排除运行时数据)
  82. const MEMORY_TEMPLATE_WHITELIST = new Set([
  83. 'stage-1-output.json',
  84. 'stage-2-output.json',
  85. 'stage-2-competitor-output.json',
  86. 'stage-3-output.json',
  87. 'stage-4-output.json',
  88. 'stage-5-output.json',
  89. 'workshop-progress.json',
  90. 'brand-context.json',
  91. 'calibration-notes.json',
  92. 'analysis-request.json'
  93. ]);
  94. // 测试技能黑名单
  95. const SKILL_BLACKLIST = new Set(['test-billing', 'test-skill']);
  96. const SKILL_SOURCE_DIRS = [
  97. 'voc', 'social-media', 'douyin', 'xiaohongshu', 'review-analysis',
  98. 'competitor-analysis', 'synthesis', 'social-voc', 'workshop',
  99. SKILLS_DIR
  100. ].map(dir => path.isAbsolute(dir) ? dir : path.join(OPENCLAW_SKILLS_ROOT, dir));
  101. // ============================================
  102. // 套件定义(技能名列表)
  103. // ============================================
  104. const PACKAGES = {
  105. 'voc-core': {
  106. label: '🔍 VOC 核心套件',
  107. description: 'Amazon 品类洞察 + 评论分析 + 竞品分析 + 数据合成 + 社媒VOC + 工作坊',
  108. skills: [
  109. // voc
  110. 'asin-reverse-keywords', 'asin-sales-volume', 'category-products', 'category-tree',
  111. 'keyword-product-ranking', 'keyword-search', 'keyword-search-trend',
  112. 'product-detail-query', 'product-monitor', 'product-reviews-query', 'product-search', 'similar-products',
  113. 'voc-single-platform-report', 'voc-data-normalizer', 'voc-platform-mini-report-generator',
  114. 'voc-html-report-generator', 'voc-report-auditor', 'voc-chapter-insight-writer',
  115. // review-analysis
  116. 'review-batch-collection', 'review-highlight-extraction', 'review-keyword-cloud',
  117. 'review-pain-point-extraction', 'review-sentiment-analysis',
  118. // competitor-analysis
  119. 'competitor-bsr-tracking', 'competitor-discovery', 'competitor-pricing-analysis', 'competitor-product-comparison',
  120. // synthesis
  121. 'brand-profile', 'category-landscape', 'html-report-generator', 'product-deep-analysis', 'user-persona', 'voc-proposal',
  122. // social-voc
  123. 'instagram-brand-voc', 'social-trend-analysis', 'tiktok-brand-voc', 'tiktok-category-voc',
  124. // workshop
  125. 'brand-context-builder', 'voc-token-preflight'
  126. ]
  127. },
  128. 'voc-social-data': {
  129. label: '📱 VOC 社媒数据套件',
  130. description: '小红书 / 抖音 / TikTok / Instagram 数据采集',
  131. aliases: ['social-media'],
  132. skills: [
  133. // social-media
  134. 'instagram-search', 'instagram-user-info', 'instagram-user-posts',
  135. 'tiktok-hashtag-detail', 'tiktok-hashtag-videos', 'tiktok-user-posts',
  136. 'tiktok-user-profile', 'tiktok-user-search', 'tiktok-video-comments',
  137. 'tiktok-video-detail', 'tiktok-video-search',
  138. // douyin
  139. 'douyin-comment-replies', 'douyin-general-search', 'douyin-hashtag-search',
  140. 'douyin-mini-voc',
  141. 'douyin-user-profile', 'douyin-user-search', 'douyin-user-posts', 'douyin-video-comments', 'douyin-video-detail',
  142. 'douyin-speaking-profile-builder', 'douyin-speaking-profile-memory', 'douyin-speaking-keyword-monitor', 'douyin-speaking-account-monitor',
  143. 'douyin-speaking-daily-runner',
  144. 'douyin-speaking-daily-report', 'douyin-viral-script-analyzer', 'douyin-video-transcript',
  145. // xiaohongshu
  146. 'xiaohongshu-mini-voc', 'xiaohongshu-note-comments', 'xiaohongshu-note-detail',
  147. 'xiaohongshu-search-notes', 'xiaohongshu-user-info'
  148. ]
  149. },
  150. 'douyin-speaking-daily': {
  151. label: '🎙️ 抖音口播每日稿件日报技能包',
  152. description: '抖音口播 1.1 爆款内容洞察闭环:问答建档、关键词监听、爆款拆解、逐字稿转写、每日稿件日报',
  153. bundleDir: 'douyin-speaking-daily',
  154. skills: [
  155. 'voc-token-preflight',
  156. 'douyin-general-search', 'douyin-hashtag-search',
  157. 'douyin-video-detail', 'douyin-video-comments', 'douyin-comment-replies',
  158. 'douyin-user-search', 'douyin-user-profile', 'douyin-user-posts', 'douyin-mini-voc',
  159. 'douyin-speaking-profile-builder', 'douyin-speaking-profile-memory', 'douyin-speaking-keyword-monitor', 'douyin-speaking-account-monitor',
  160. 'douyin-speaking-daily-runner',
  161. 'douyin-speaking-daily-report', 'douyin-viral-script-analyzer', 'douyin-video-transcript'
  162. ]
  163. },
  164. 'xiaohongshu-platform': {
  165. label: '📕 小红书平台 VOC 套件',
  166. description: '小红书数据采集 + 小红书 mini VOC + 单平台 VOC 报告生成工具链',
  167. skills: [
  168. 'xiaohongshu-search-notes', 'xiaohongshu-note-detail', 'xiaohongshu-note-comments',
  169. 'xiaohongshu-user-info', 'xiaohongshu-mini-voc',
  170. 'voc-single-platform-report', 'voc-data-normalizer', 'voc-platform-mini-report-generator',
  171. 'voc-html-report-generator', 'voc-report-auditor', 'voc-chapter-insight-writer',
  172. 'voc-token-preflight'
  173. ]
  174. }
  175. };
  176. const PACKAGE_ALIAS_TO_CANONICAL = Object.fromEntries(
  177. Object.entries(PACKAGES).flatMap(([pkgId, pkg]) =>
  178. (pkg.aliases || []).map(alias => [alias, pkgId])
  179. )
  180. );
  181. function getPackageDefinition(pkgId) {
  182. return PACKAGES[pkgId] || PACKAGES[PACKAGE_ALIAS_TO_CANONICAL[pkgId]];
  183. }
  184. function isLegacyPackageId(pkgId) {
  185. return Boolean(PACKAGE_ALIAS_TO_CANONICAL[pkgId]);
  186. }
  187. function getPackageLogLabel(pkgId) {
  188. const pkg = getPackageDefinition(pkgId);
  189. if (!pkg) return pkgId;
  190. const suffix = isLegacyPackageId(pkgId) ? `(兼容旧名 ${pkgId})` : '';
  191. return `${pkg.label}${suffix}`;
  192. }
  193. function parseArgs(argv) {
  194. const options = {
  195. only: [],
  196. skipWorkshop: false,
  197. skipAll: false,
  198. skipUpload: false,
  199. help: false
  200. };
  201. for (let i = 0; i < argv.length; i++) {
  202. const token = argv[i];
  203. if (token === '--only') {
  204. options.only = (argv[++i] || '').split(',').map(v => v.trim()).filter(Boolean);
  205. } else if (token === '--skip-workshop') {
  206. options.skipWorkshop = true;
  207. } else if (token === '--skip-all') {
  208. options.skipAll = true;
  209. } else if (token === '--skip-upload') {
  210. options.skipUpload = true;
  211. } else if (token === '--help' || token === '-h') {
  212. options.help = true;
  213. }
  214. }
  215. return options;
  216. }
  217. function showHelp() {
  218. console.log([
  219. 'Usage:',
  220. ' node scripts/deploy/package-and-upload.js',
  221. ' node scripts/deploy/package-and-upload.js --only xiaohongshu-platform --skip-workshop --skip-all',
  222. '',
  223. 'Options:',
  224. ' --only a,b,c 只打包/上传指定套件',
  225. ' --skip-workshop 跳过 workshop.zip',
  226. ' --skip-all 跳过 openclaw-all.zip',
  227. ' --skip-upload 只生成本地 zip,不上传 CDN',
  228. ' --help, -h 显示帮助'
  229. ].join('\n'));
  230. }
  231. function getSelectedPackageEntries(only) {
  232. if (!only || only.length === 0) {
  233. return Object.entries(PACKAGES).map(([pkgId, pkg]) => [pkgId, pkg, pkgId]);
  234. }
  235. return only.map(pkgId => {
  236. const pkg = getPackageDefinition(pkgId);
  237. if (!pkg) {
  238. throw new Error(`未知套件: ${pkgId}`);
  239. }
  240. const canonicalId = PACKAGE_ALIAS_TO_CANONICAL[pkgId] || pkgId;
  241. return [pkgId, pkg, canonicalId];
  242. });
  243. }
  244. function findSkillSourceDir(skillName) {
  245. for (const sourceRoot of SKILL_SOURCE_DIRS) {
  246. const candidate = path.join(sourceRoot, skillName);
  247. if (fs.existsSync(candidate) && fs.existsSync(path.join(candidate, 'SKILL.md'))) {
  248. return candidate;
  249. }
  250. }
  251. return undefined;
  252. }
  253. function copySkillFiles(srcDir, destDir) {
  254. fs.mkdirSync(destDir, { recursive: true });
  255. for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
  256. if (
  257. entry.name.includes('.bak.')
  258. || entry.name.endsWith('~')
  259. || entry.name.endsWith('.tmp')
  260. ) {
  261. continue;
  262. }
  263. const srcPath = path.join(srcDir, entry.name);
  264. const destPath = path.join(destDir, entry.name);
  265. if (entry.isDirectory()) {
  266. copySkillFiles(srcPath, destPath);
  267. } else if (entry.isFile()) {
  268. fs.copyFileSync(srcPath, destPath);
  269. }
  270. }
  271. }
  272. function copyDirRecursive(srcDir, destDir) {
  273. fs.mkdirSync(destDir, { recursive: true });
  274. let copied = 0;
  275. for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
  276. const srcPath = path.join(srcDir, entry.name);
  277. const destPath = path.join(destDir, entry.name);
  278. if (entry.isDirectory()) {
  279. copied += copyDirRecursive(srcPath, destPath);
  280. } else if (entry.isFile()) {
  281. fs.copyFileSync(srcPath, destPath);
  282. copied++;
  283. }
  284. }
  285. return copied;
  286. }
  287. // ============================================
  288. // 七牛上传
  289. // ============================================
  290. function uploadFile(localFile, cdnKey) {
  291. return new Promise((resolve, reject) => {
  292. const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);
  293. const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 });
  294. const token = putPolicy.uploadToken(mac);
  295. const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 }));
  296. const putExtra = new qiniu.form_up.PutExtra();
  297. formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => {
  298. if (err) return reject(err);
  299. if (info.statusCode === 200) {
  300. resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` });
  301. } else {
  302. reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`));
  303. }
  304. });
  305. });
  306. }
  307. function copyToolPackageFiles(pkgDir) {
  308. const toolsDir = path.join(pkgDir, 'tools');
  309. fs.mkdirSync(toolsDir, { recursive: true });
  310. let copied = 0;
  311. let missing = 0;
  312. for (const file of TOOL_PACKAGE_FILES) {
  313. if (!fs.existsSync(file.src)) {
  314. missing++;
  315. continue;
  316. }
  317. const dest = path.join(toolsDir, file.dest);
  318. fs.mkdirSync(path.dirname(dest), { recursive: true });
  319. fs.copyFileSync(file.src, dest);
  320. copied++;
  321. }
  322. return { copied, missing };
  323. }
  324. // ============================================
  325. // 打包逻辑
  326. // ============================================
  327. function buildPackage(pkgId, pkg, outputPkgId = pkgId) {
  328. const pkgDir = path.join(TEMP_BASE, outputPkgId);
  329. const skillsDir = path.join(pkgDir, 'skills');
  330. // 清理 + 创建目录
  331. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  332. fs.mkdirSync(skillsDir, { recursive: true });
  333. // 复制 install.js
  334. if (fs.existsSync(INSTALL_JS)) {
  335. fs.copyFileSync(INSTALL_JS, path.join(pkgDir, 'install.js'));
  336. }
  337. // 复制 set-voc-token.js(Token 设置工具)
  338. if (fs.existsSync(SET_VOC_TOKEN_JS)) {
  339. fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
  340. }
  341. // 复制 voc-token-preflight.js(Token 预飞检测工具)
  342. if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
  343. fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
  344. }
  345. const toolCopyResult = copyToolPackageFiles(pkgDir);
  346. let bundleFileCount = 0;
  347. if (pkg.bundleDir) {
  348. const bundleSrc = path.join(OPENCLAW_SKILLS_ROOT, pkg.bundleDir);
  349. if (fs.existsSync(bundleSrc)) {
  350. bundleFileCount = copyDirRecursive(bundleSrc, path.join(pkgDir, pkg.bundleDir));
  351. } else {
  352. console.log(` ⚠️ 套件目录不存在,跳过: ${pkg.bundleDir}`);
  353. }
  354. }
  355. // 生成 README.txt
  356. const readme = [
  357. `OpenClaw ${pkg.label}`,
  358. `${'='.repeat(50)}`,
  359. '',
  360. pkg.description,
  361. '',
  362. `包含 ${pkg.skills.length} 个技能`,
  363. `包含 ${toolCopyResult.copied} 个本地工具文件(安装到 ~/.openclaw/tools 和 ~/.openclaw/workspace/scripts/tools)`,
  364. ...(pkg.bundleDir ? [`包含套件入口目录: ${pkg.bundleDir} (${bundleFileCount} 个文件)`] : []),
  365. '',
  366. '安装方法:',
  367. ' 1. 确保已安装 OpenClaw',
  368. ' 2. 解压本压缩包',
  369. ' 3. 在解压目录运行: node install.js',
  370. ' 4. 设置 Token: node ~/.openclaw/tools/set-voc-token.js <your-session-token>',
  371. ' 5. 重启 OpenClaw gateway',
  372. '',
  373. '其他命令:',
  374. ' node install.js --list # 列出所有技能',
  375. ' node install.js --dry-run # 预览模式',
  376. ' node install.js --only a,b,c # 只安装指定技能',
  377. ' node install.js --skip a,b,c # 跳过指定技能',
  378. ' node ~/.openclaw/tools/set-voc-token.js <token> # 设置/更新 VOC Token',
  379. ' node ~/.openclaw/tools/voc-token-preflight.js # 检测 Token 有效性和余额',
  380. '',
  381. '技能列表:',
  382. ...pkg.skills.map(s => ` - ${s}`),
  383. ''
  384. ].join('\n');
  385. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  386. // 复制每个技能
  387. let copied = 0;
  388. let missing = 0;
  389. for (const skillName of pkg.skills) {
  390. const srcDir = findSkillSourceDir(skillName);
  391. if (!srcDir) {
  392. console.log(` ⚠️ 跳过 ${skillName}(未找到源码或已安装目录)`);
  393. missing++;
  394. continue;
  395. }
  396. const destDir = path.join(skillsDir, skillName);
  397. copySkillFiles(srcDir, destDir);
  398. copied++;
  399. }
  400. console.log(` ✅ 复制 ${copied} 个技能(跳过 ${missing} 个),${toolCopyResult.copied} 个工具文件` +
  401. (toolCopyResult.missing > 0 ? `(工具缺失 ${toolCopyResult.missing} 个)` : '') +
  402. (pkg.bundleDir ? `,套件入口 ${bundleFileCount} 个文件` : ''));
  403. // 压缩
  404. const zipPath = path.join(DIST_DIR, `${outputPkgId}.zip`);
  405. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  406. // 使用 PowerShell Compress-Archive
  407. const absZipPath = path.resolve(zipPath);
  408. const absPkgDir = path.resolve(pkgDir);
  409. execSync(
  410. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  411. { stdio: 'inherit' }
  412. );
  413. const zipSize = fs.statSync(zipPath).size;
  414. console.log(` 📦 ${path.basename(zipPath)} (${(zipSize / 1024).toFixed(0)} KB)`);
  415. return zipPath;
  416. }
  417. // ============================================
  418. // Workshop 打包逻辑
  419. // ============================================
  420. function buildWorkshopPackage() {
  421. const pkgDir = path.join(TEMP_BASE, 'workshop');
  422. const memoryDir = path.join(pkgDir, 'memory-templates');
  423. const skillsDir = path.join(pkgDir, 'skills');
  424. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  425. fs.mkdirSync(memoryDir, { recursive: true });
  426. fs.mkdirSync(skillsDir, { recursive: true });
  427. // 复制安装脚本
  428. if (fs.existsSync(INSTALL_WORKSHOP_JS)) {
  429. fs.copyFileSync(INSTALL_WORKSHOP_JS, path.join(pkgDir, 'install-workshop.js'));
  430. }
  431. // 复制 Token 工具(workshop 自足所需)
  432. if (fs.existsSync(SET_VOC_TOKEN_JS)) {
  433. fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
  434. }
  435. if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
  436. fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
  437. }
  438. const toolCopyResult = copyToolPackageFiles(pkgDir);
  439. // 复制凭证模板
  440. if (fs.existsSync(CREDENTIALS_TEMPLATE)) {
  441. fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json'));
  442. }
  443. let copied = 0;
  444. // 复制 workshop 文件(仅白名单 playbooks)
  445. if (fs.existsSync(WORKSHOP_DIR)) {
  446. const mdFiles = fs.readdirSync(WORKSHOP_DIR).filter(f =>
  447. f.endsWith('.md') && WORKSHOP_MD_WHITELIST.has(f) && fs.statSync(path.join(WORKSHOP_DIR, f)).isFile()
  448. );
  449. for (const f of mdFiles) {
  450. fs.copyFileSync(path.join(WORKSHOP_DIR, f), path.join(pkgDir, f));
  451. copied++;
  452. }
  453. }
  454. // 复制 memory templates(仅白名单模板)
  455. if (fs.existsSync(MEMORY_TEMPLATES_DIR)) {
  456. const jsonFiles = fs.readdirSync(MEMORY_TEMPLATES_DIR).filter(f =>
  457. f.endsWith('.json') && MEMORY_TEMPLATE_WHITELIST.has(f) && fs.statSync(path.join(MEMORY_TEMPLATES_DIR, f)).isFile()
  458. );
  459. for (const f of jsonFiles) {
  460. fs.copyFileSync(path.join(MEMORY_TEMPLATES_DIR, f), path.join(memoryDir, f));
  461. copied++;
  462. }
  463. }
  464. // 复制 workshop 所需的全部 skills(复用 voc-core 列表,workshop 主线+侧线都依赖它们)
  465. const workshopSkills = PACKAGES['voc-core'].skills;
  466. let skillsCopied = 0;
  467. let skillsMissing = 0;
  468. for (const skillName of workshopSkills) {
  469. const srcDir = path.join(SKILLS_DIR, skillName);
  470. if (!fs.existsSync(srcDir)) {
  471. skillsMissing++;
  472. continue;
  473. }
  474. const destDir = path.join(skillsDir, skillName);
  475. fs.mkdirSync(destDir, { recursive: true });
  476. const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
  477. for (const f of files) {
  478. fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
  479. }
  480. skillsCopied++;
  481. }
  482. // 生成 README.txt
  483. const readme = [
  484. 'OpenClaw 🦐 VOC虾工作坊 v4.2(自足版)',
  485. '==================================================',
  486. '',
  487. '一个压缩包搞定所有!包含:',
  488. ` 🔧 ${skillsCopied} 个 workshop 必需的 skills(品类/评论/竞品/合成/社媒VOC/工作坊)`,
  489. ' 📘 11 份 playbook(入口+全局规则+6个Session+配套文档)',
  490. ' 📄 9 份 memory templates(记忆模板)',
  491. ` 🔑 ${toolCopyResult.copied} 个本地工具文件(Token 工具 + VOC 报告脚本)`,
  492. ' 📝 1 份凭证模板(voc-credentials.template.json)',
  493. '',
  494. '架构:6个Session + 17个Side Quest,主线感知 + 支线深耕',
  495. '',
  496. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  497. '📦 一键安装:',
  498. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  499. ' 1. 解压本压缩包',
  500. ' 2. 运行: node install-workshop.js',
  501. ' (自动安装 skills + playbooks + memory templates + tools + 凭证模板)',
  502. ' 3. 设置 Token(首次使用):',
  503. ' a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF',
  504. ' b. 登录并充值(最低 ¥0.01 体验 1 次)',
  505. ' c. 复制 session token(格式 r:xxxx...)',
  506. ' d. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>',
  507. ' 4. 重启 OpenClaw gateway',
  508. ' 5. 对话框说"开始 VOC 工作坊",Agent 会自动进入 Session 0',
  509. '',
  510. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  511. '🔍 验证安装:',
  512. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  513. ' node ~/.openclaw/tools/voc-token-preflight.js',
  514. ' (期望看到: status=valid, 余额 >= 1)',
  515. '',
  516. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  517. '🛠 其他命令:',
  518. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  519. ' node install-workshop.js --dry-run # 预览将要安装的文件',
  520. ' node install-workshop.js --skills-only # 只装 skills',
  521. ' node install-workshop.js --playbook-only # 只装 playbooks (旧行为)',
  522. '',
  523. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  524. '📂 文件结构:',
  525. '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
  526. ' skills/ ← workshop 用到的 skills',
  527. ' voc-token-preflight/ ← Session 0/1 的 Token 预飞闸门',
  528. ' brand-context-builder/ ← Session 0 品牌建档',
  529. ' category-landscape/ ← Session 1 品类全景',
  530. ' keyword-search/, category-tree/, ... (其余 Amazon/VOC/合成 skills)',
  531. ' workshop-voc-playbook.md ← 入口路由',
  532. ' workshop-global-rules.md ← 全局规则 + Token 预飞协议',
  533. ' workshop-session-0-intake.md ← Session 0: 品牌建档 + Token 预飞',
  534. ' workshop-session-1-category.md ← Session 1: 品类 + 4 Side Quests',
  535. ' workshop-session-2-brand.md ← Session 2: 品牌 + 3 Side Quests',
  536. ' workshop-session-3-voc.md ← Session 3: VOC + 3 Side Quests',
  537. ' workshop-session-4-deep.md ← Session 4: 深入 + 5 Side Quests',
  538. ' workshop-session-5-report.md ← Session 5: 报告 + 2 Side Quests',
  539. ' memory-templates/*.json ← 记忆模板(9个)',
  540. ' tools/* ← 安装到 ~/.openclaw/tools 和 ~/.openclaw/workspace/scripts/tools',
  541. ' set-voc-token.js ← Token 写入工具',
  542. ' voc-token-preflight.js ← Token 预飞检测工具',
  543. ' voc-credentials.template.json ← 凭证模板',
  544. ''
  545. ].join('\n');
  546. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  547. console.log(` ✅ 复制 ${copied} 个 playbook/template + ${skillsCopied} 个 skills + ${toolCopyResult.copied} 个工具文件` +
  548. (skillsMissing > 0 ? ` (缺失 ${skillsMissing})` : '') +
  549. (toolCopyResult.missing > 0 ? ` (工具缺失 ${toolCopyResult.missing})` : ''));
  550. // 压缩
  551. const zipPath = path.join(DIST_DIR, 'workshop.zip');
  552. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  553. const absZipPath = path.resolve(zipPath);
  554. const absPkgDir = path.resolve(pkgDir);
  555. execSync(
  556. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  557. { stdio: 'inherit' }
  558. );
  559. const zipSize = fs.statSync(zipPath).size;
  560. console.log(` 📦 workshop.zip (${(zipSize / 1024).toFixed(0)} KB)`);
  561. return zipPath;
  562. }
  563. // ============================================
  564. // 全量包打包逻辑
  565. // ============================================
  566. function buildAllPackage() {
  567. const pkgDir = path.join(TEMP_BASE, 'openclaw-all');
  568. const skillsDir = path.join(pkgDir, 'skills');
  569. const workshopDir = path.join(pkgDir, 'workshop');
  570. const memoryDir = path.join(workshopDir, 'memory-templates');
  571. if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true });
  572. fs.mkdirSync(memoryDir, { recursive: true });
  573. // 复制 install-all.js
  574. if (fs.existsSync(INSTALL_ALL_JS)) {
  575. fs.copyFileSync(INSTALL_ALL_JS, path.join(pkgDir, 'install-all.js'));
  576. }
  577. // 复制 set-voc-token.js
  578. if (fs.existsSync(SET_VOC_TOKEN_JS)) {
  579. fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js'));
  580. }
  581. // 复制 voc-token-preflight.js
  582. if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) {
  583. fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js'));
  584. }
  585. const toolCopyResult = copyToolPackageFiles(pkgDir);
  586. // 复制凭证模板
  587. if (fs.existsSync(CREDENTIALS_TEMPLATE)) {
  588. fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json'));
  589. }
  590. let skillCount = 0;
  591. let workshopCount = 0;
  592. // 复制发布套件中的技能(排除测试技能和未发布的即梦/视频创作技能)
  593. const RELEASED_SKILLS = new Set([
  594. ...PACKAGES['voc-core'].skills,
  595. ...PACKAGES['voc-social-data'].skills
  596. ]);
  597. if (fs.existsSync(SKILLS_DIR)) {
  598. const allSkills = fs.readdirSync(SKILLS_DIR).filter(d => {
  599. const p = path.join(SKILLS_DIR, d);
  600. return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'))
  601. && !SKILL_BLACKLIST.has(d) && RELEASED_SKILLS.has(d);
  602. });
  603. for (const skill of allSkills) {
  604. const srcDir = path.join(SKILLS_DIR, skill);
  605. const destDir = path.join(skillsDir, skill);
  606. fs.mkdirSync(destDir, { recursive: true });
  607. const files = fs.readdirSync(srcDir).filter(f => fs.statSync(path.join(srcDir, f)).isFile());
  608. for (const f of files) {
  609. fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
  610. }
  611. skillCount++;
  612. }
  613. }
  614. console.log(` ✅ 复制 ${skillCount} 个技能 + ${toolCopyResult.copied} 个工具文件` +
  615. (toolCopyResult.missing > 0 ? `(工具缺失 ${toolCopyResult.missing} 个)` : ''));
  616. // 复制 workshop 文件(仅白名单)
  617. if (fs.existsSync(WORKSHOP_DIR)) {
  618. const mdFiles = fs.readdirSync(WORKSHOP_DIR).filter(f =>
  619. f.endsWith('.md') && WORKSHOP_MD_WHITELIST.has(f) && fs.statSync(path.join(WORKSHOP_DIR, f)).isFile()
  620. );
  621. for (const f of mdFiles) {
  622. fs.copyFileSync(path.join(WORKSHOP_DIR, f), path.join(workshopDir, f));
  623. workshopCount++;
  624. }
  625. }
  626. if (fs.existsSync(MEMORY_TEMPLATES_DIR)) {
  627. const jsonFiles = fs.readdirSync(MEMORY_TEMPLATES_DIR).filter(f =>
  628. f.endsWith('.json') && MEMORY_TEMPLATE_WHITELIST.has(f) && fs.statSync(path.join(MEMORY_TEMPLATES_DIR, f)).isFile()
  629. );
  630. for (const f of jsonFiles) {
  631. fs.copyFileSync(path.join(MEMORY_TEMPLATES_DIR, f), path.join(memoryDir, f));
  632. workshopCount++;
  633. }
  634. }
  635. console.log(` ✅ 复制 ${workshopCount} 个 workshop 文件`);
  636. // README.txt
  637. const readme = [
  638. 'OpenClaw 全量安装包 v4.1',
  639. '==================================================',
  640. '',
  641. `包含 ${skillCount} 个技能 + VOC虾工作坊 (${workshopCount}个文件) + ${toolCopyResult.copied} 个本地工具文件`,
  642. '',
  643. '安装方法:',
  644. ' 1. 解压本压缩包',
  645. ' 2. 在解压目录运行: node install-all.js',
  646. ' 3. 设置 Token:',
  647. ' a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF',
  648. ' b. 登录并充值',
  649. ' c. 获取 session token (r:xxxx...)',
  650. ' d. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>',
  651. ' 4. 重启 OpenClaw gateway',
  652. '',
  653. '其他命令:',
  654. ' node install-all.js --list # 列出包含内容',
  655. ' node install-all.js --dry-run # 预览模式',
  656. ' node install-all.js --skills-only # 只装 Skills',
  657. ' node install-all.js --workshop-only # 只装 Workshop',
  658. ' node ~/.openclaw/tools/set-voc-token.js <token> # 设置/更新 Token',
  659. ' node ~/.openclaw/tools/voc-token-preflight.js # 检测 Token 有效性和余额(Workshop 前置检查)',
  660. ''
  661. ].join('\n');
  662. fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8');
  663. // 压缩
  664. const zipPath = path.join(DIST_DIR, 'openclaw-all.zip');
  665. if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
  666. const absZipPath = path.resolve(zipPath);
  667. const absPkgDir = path.resolve(pkgDir);
  668. execSync(
  669. `powershell -Command "Compress-Archive -Path '${absPkgDir}\\*' -DestinationPath '${absZipPath}' -Force"`,
  670. { stdio: 'inherit' }
  671. );
  672. const zipSize = fs.statSync(zipPath).size;
  673. console.log(` 📦 openclaw-all.zip (${(zipSize / 1024).toFixed(0)} KB)`);
  674. return zipPath;
  675. }
  676. // ============================================
  677. // Main
  678. // ============================================
  679. async function main() {
  680. const options = parseArgs(process.argv.slice(2));
  681. if (options.help) {
  682. showHelp();
  683. return;
  684. }
  685. console.log('');
  686. console.log('==================================================');
  687. console.log(' OpenClaw Skills + Workshop 打包 + CDN 上传 v4.1');
  688. console.log('==================================================');
  689. console.log('');
  690. // 前置检查
  691. if (!SKILL_SOURCE_DIRS.some(dir => fs.existsSync(dir))) {
  692. console.error(`❌ 未找到可用 Skills 源目录`);
  693. console.error(` 已检查: ${SKILL_SOURCE_DIRS.join(', ')}`);
  694. process.exit(1);
  695. }
  696. if (!fs.existsSync(INSTALL_JS)) {
  697. console.error(`❌ install.js 不存在: ${INSTALL_JS}`);
  698. process.exit(1);
  699. }
  700. fs.mkdirSync(DIST_DIR, { recursive: true });
  701. fs.mkdirSync(TEMP_BASE, { recursive: true });
  702. // Step 1: 打包 Skills
  703. console.log('── Step 1: 打包 Skills ──');
  704. const zipFiles = {};
  705. const selectedPackageEntries = getSelectedPackageEntries(options.only);
  706. for (const [pkgId, pkg, canonicalId] of selectedPackageEntries) {
  707. console.log(` ${pkg.label} (${pkg.skills.length} 个技能)`);
  708. zipFiles[pkgId] = buildPackage(canonicalId, pkg, pkgId);
  709. if (options.only.length > 0) continue;
  710. for (const alias of pkg.aliases || []) {
  711. console.log(` ${pkg.label}(兼容旧名 ${alias},${pkg.skills.length} 个技能)`);
  712. zipFiles[alias] = buildPackage(canonicalId, pkg, alias);
  713. }
  714. }
  715. console.log('');
  716. // Step 1b: 打包 Workshop
  717. if (!options.skipWorkshop) {
  718. console.log('── Step 1b: 打包 Workshop ──');
  719. if (fs.existsSync(WORKSHOP_DIR)) {
  720. console.log(' 🦐 VOC虾工作坊');
  721. zipFiles['workshop'] = buildWorkshopPackage();
  722. } else {
  723. console.log(' ⚠️ Workshop 目录不存在,跳过');
  724. }
  725. console.log('');
  726. }
  727. // Step 1c: 打包全量包
  728. if (!options.skipAll) {
  729. console.log('── Step 1c: 打包全量包 ──');
  730. console.log(' 📦 openclaw-all (Skills + Workshop + Token工具)');
  731. zipFiles['openclaw-all'] = buildAllPackage();
  732. console.log('');
  733. }
  734. if (options.skipUpload) {
  735. console.log('── Step 2: 跳过上传 ──');
  736. for (const [pkgId, zipPath] of Object.entries(zipFiles)) {
  737. console.log(` ${pkgId}: ${zipPath}`);
  738. }
  739. if (fs.existsSync(TEMP_BASE)) fs.rmSync(TEMP_BASE, { recursive: true, force: true });
  740. return;
  741. }
  742. // Step 2: 上传到七牛
  743. console.log('── Step 2: 上传到七牛云 CDN ──');
  744. const downloadUrls = {};
  745. for (const [pkgId, zipPath] of Object.entries(zipFiles)) {
  746. const pkg = getPackageDefinition(pkgId);
  747. const cdnKey = `${CDN_PREFIX}/${pkgId}.zip`;
  748. const label = pkgId === 'workshop' ? '🦐 VOC虾工作坊' : pkgId === 'openclaw-all' ? '📦 全量包' : getPackageLogLabel(pkgId);
  749. process.stdout.write(` ${label} ... `);
  750. try {
  751. const result = await uploadFile(zipPath, cdnKey);
  752. downloadUrls[pkgId] = result.url;
  753. console.log(`✅ ${result.url}`);
  754. } catch (err) {
  755. console.log(`❌ ${err.message}`);
  756. }
  757. }
  758. console.log('');
  759. // 输出结果
  760. console.log('==================================================');
  761. console.log(' 📥 下载链接');
  762. console.log('==================================================');
  763. console.log('');
  764. for (const [pkgId, url] of Object.entries(downloadUrls)) {
  765. if (pkgId === 'workshop') {
  766. console.log(' 🦐 VOC虾工作坊');
  767. } else if (pkgId === 'openclaw-all') {
  768. console.log(' 📦 全量包');
  769. } else {
  770. const pkg = getPackageDefinition(pkgId);
  771. if (pkg) console.log(` ${pkg.label}`);
  772. }
  773. console.log(` 👉 ${url}`);
  774. console.log('');
  775. }
  776. // 保存到文件
  777. const outputPath = path.join(DIST_DIR, 'download-urls.json');
  778. fs.writeFileSync(outputPath, JSON.stringify(downloadUrls, null, 2), 'utf8');
  779. console.log(` 💾 链接已保存到: ${outputPath}`);
  780. console.log('');
  781. // 清理临时目录
  782. if (fs.existsSync(TEMP_BASE)) fs.rmSync(TEMP_BASE, { recursive: true, force: true });
  783. }
  784. main().catch(err => {
  785. console.error('❌ 出错:', err);
  786. process.exit(1);
  787. });