#!/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 PROJECT_ROOT = path.join(__dirname, '..', '..'); const OPENCLAW_SKILLS_ROOT = path.join(PROJECT_ROOT, 'openclaw-skills'); const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills'); const INSTALL_JS = path.join(__dirname, '..', '..', 'dist', 'install.js'); const SET_VOC_TOKEN_JS = path.join(__dirname, '..', 'tools', 'set-voc-token.js'); const VOC_TOKEN_PREFLIGHT_JS = path.join(__dirname, '..', 'tools', 'voc-token-preflight.js'); const INSTALL_WORKSHOP_JS = path.join(__dirname, 'install-workshop.js'); const INSTALL_ALL_JS = path.join(__dirname, 'install-all.js'); const CREDENTIALS_TEMPLATE = path.join(__dirname, '..', '..', '__config', 'voc-credentials.template.json'); const DIST_DIR = path.join(__dirname, '..', '..', 'dist'); const TEMP_BASE = path.join(os.tmpdir(), 'openclaw-pkg'); const WORKSHOP_DIR = path.join(os.homedir(), '.openclaw', 'workspace'); const MEMORY_TEMPLATES_DIR = path.join(WORKSHOP_DIR, 'memory'); const TOOL_PACKAGE_FILES = [ { src: path.join(__dirname, '..', 'tools', 'openclaw-tool-runner.js'), dest: 'openclaw-tool-runner.js' }, { src: path.join(__dirname, '..', 'tools', 'set-voc-token.js'), dest: 'set-voc-token.js' }, { src: path.join(__dirname, '..', 'tools', 'voc-token-preflight.js'), dest: 'voc-token-preflight.js' }, { src: path.join(__dirname, '..', 'tools', 'voc-single-platform-report.js'), dest: 'voc-single-platform-report.js' }, { src: path.join(__dirname, '..', 'tools', 'voc-data-normalizer.js'), dest: 'voc-data-normalizer.js' }, { src: path.join(__dirname, '..', 'tools', 'platform-mini-report-generator.js'), dest: 'platform-mini-report-generator.js' }, { src: path.join(__dirname, '..', 'tools', 'chapter-insight-writer.js'), dest: 'chapter-insight-writer.js' }, { src: path.join(__dirname, '..', 'tools', 'voc-report-auditor.js'), dest: 'voc-report-auditor.js' }, { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-profile-memory.js'), dest: 'douyin-speaking-profile-memory.js' }, { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-daily-runner.js'), dest: 'douyin-speaking-daily-runner.js' }, { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-daily-report.js'), dest: 'douyin-speaking-daily-report.js' }, { src: path.join(__dirname, '..', 'tools', 'douyin-speaking-daily-p1-smoke.js'), dest: 'douyin-speaking-daily-p1-smoke.js' }, { src: path.join(__dirname, '..', 'tools', 'douyin-viral-script-analyzer.js'), dest: 'douyin-viral-script-analyzer.js' }, { src: path.join(__dirname, '..', 'tools', 'douyin-video-transcriber.js'), dest: 'douyin-video-transcriber.js' }, { 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') }, { 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') }, { src: path.join(__dirname, '..', '..', 'demo', 'xiaohongshu', 'raw-notes.json'), dest: path.join('course-samples', 'xiaohongshu', 'raw-notes.json') }, { src: path.join(__dirname, '..', '..', 'demo', 'xiaohongshu', 'raw-comments.json'), dest: path.join('course-samples', 'xiaohongshu', 'raw-comments.json') } ]; // OpenClaw 系统文件(不能打包,否则覆盖用户配置) const SYSTEM_MD_FILES = new Set([ 'AGENTS.md', 'BOOTSTRAP.md', 'HEARTBEAT.md', 'IDENTITY.md', 'SOUL.md', 'TOOLS.md', 'USER.md' ]); // Workshop playbook 白名单(只打包这些 .md) const WORKSHOP_MD_WHITELIST = new Set([ 'workshop-voc-playbook.md', 'workshop-global-rules.md', 'workshop-session-0-intake.md', 'workshop-session-1-category.md', 'workshop-session-2-brand.md', 'workshop-session-3-voc.md', 'workshop-session-4-deep.md', 'workshop-session-5-report.md', 'product-analysis-playbook.md', 'synthesis-prompts.md', 'voc-report-schema-spec.md' ]); // Memory template 白名单(排除运行时数据) const MEMORY_TEMPLATE_WHITELIST = new Set([ 'stage-1-output.json', 'stage-2-output.json', 'stage-2-competitor-output.json', 'stage-3-output.json', 'stage-4-output.json', 'stage-5-output.json', 'workshop-progress.json', 'brand-context.json', 'calibration-notes.json', 'analysis-request.json' ]); // 测试技能黑名单 const SKILL_BLACKLIST = new Set(['test-billing', 'test-skill']); const SKILL_SOURCE_DIRS = [ 'voc', 'social-media', 'douyin', 'xiaohongshu', 'review-analysis', 'competitor-analysis', 'synthesis', 'social-voc', 'workshop', SKILLS_DIR ].map(dir => path.isAbsolute(dir) ? dir : path.join(OPENCLAW_SKILLS_ROOT, dir)); // ============================================ // 套件定义(技能名列表) // ============================================ 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', 'voc-single-platform-report', 'voc-data-normalizer', 'voc-platform-mini-report-generator', 'voc-html-report-generator', 'voc-report-auditor', 'voc-chapter-insight-writer', // 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', 'voc-token-preflight' ] }, 'voc-social-data': { label: '📱 VOC 社媒数据套件', description: '小红书 / 抖音 / TikTok / Instagram 数据采集', aliases: ['social-media'], 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-mini-voc', 'douyin-user-profile', 'douyin-user-search', 'douyin-user-posts', 'douyin-video-comments', 'douyin-video-detail', 'douyin-speaking-profile-builder', 'douyin-speaking-profile-memory', 'douyin-speaking-keyword-monitor', 'douyin-speaking-account-monitor', 'douyin-speaking-daily-runner', 'douyin-speaking-daily-report', 'douyin-viral-script-analyzer', 'douyin-video-transcript', // xiaohongshu 'xiaohongshu-mini-voc', 'xiaohongshu-note-comments', 'xiaohongshu-note-detail', 'xiaohongshu-search-notes', 'xiaohongshu-user-info' ] }, 'douyin-speaking-daily': { label: '🎙️ 抖音口播每日稿件日报技能包', description: '抖音口播 1.1 爆款内容洞察闭环:问答建档、关键词监听、爆款拆解、逐字稿转写、每日稿件日报', bundleDir: 'douyin-speaking-daily', skills: [ 'voc-token-preflight', 'douyin-general-search', 'douyin-hashtag-search', 'douyin-video-detail', 'douyin-video-comments', 'douyin-comment-replies', 'douyin-user-search', 'douyin-user-profile', 'douyin-user-posts', 'douyin-mini-voc', 'douyin-speaking-profile-builder', 'douyin-speaking-profile-memory', 'douyin-speaking-keyword-monitor', 'douyin-speaking-account-monitor', 'douyin-speaking-daily-runner', 'douyin-speaking-daily-report', 'douyin-viral-script-analyzer', 'douyin-video-transcript' ] }, 'xiaohongshu-platform': { label: '📕 小红书平台 VOC 套件', description: '小红书数据采集 + 小红书 mini VOC + 单平台 VOC 报告生成工具链', skills: [ 'xiaohongshu-search-notes', 'xiaohongshu-note-detail', 'xiaohongshu-note-comments', 'xiaohongshu-user-info', 'xiaohongshu-mini-voc', 'voc-single-platform-report', 'voc-data-normalizer', 'voc-platform-mini-report-generator', 'voc-html-report-generator', 'voc-report-auditor', 'voc-chapter-insight-writer', 'voc-token-preflight' ] } }; const PACKAGE_ALIAS_TO_CANONICAL = Object.fromEntries( Object.entries(PACKAGES).flatMap(([pkgId, pkg]) => (pkg.aliases || []).map(alias => [alias, pkgId]) ) ); function getPackageDefinition(pkgId) { return PACKAGES[pkgId] || PACKAGES[PACKAGE_ALIAS_TO_CANONICAL[pkgId]]; } function isLegacyPackageId(pkgId) { return Boolean(PACKAGE_ALIAS_TO_CANONICAL[pkgId]); } function getPackageLogLabel(pkgId) { const pkg = getPackageDefinition(pkgId); if (!pkg) return pkgId; const suffix = isLegacyPackageId(pkgId) ? `(兼容旧名 ${pkgId})` : ''; return `${pkg.label}${suffix}`; } function parseArgs(argv) { const options = { only: [], skipWorkshop: false, skipAll: false, skipUpload: false, help: false }; for (let i = 0; i < argv.length; i++) { const token = argv[i]; if (token === '--only') { options.only = (argv[++i] || '').split(',').map(v => v.trim()).filter(Boolean); } else if (token === '--skip-workshop') { options.skipWorkshop = true; } else if (token === '--skip-all') { options.skipAll = true; } else if (token === '--skip-upload') { options.skipUpload = true; } else if (token === '--help' || token === '-h') { options.help = true; } } return options; } function showHelp() { console.log([ 'Usage:', ' node scripts/deploy/package-and-upload.js', ' node scripts/deploy/package-and-upload.js --only xiaohongshu-platform --skip-workshop --skip-all', '', 'Options:', ' --only a,b,c 只打包/上传指定套件', ' --skip-workshop 跳过 workshop.zip', ' --skip-all 跳过 openclaw-all.zip', ' --skip-upload 只生成本地 zip,不上传 CDN', ' --help, -h 显示帮助' ].join('\n')); } function getSelectedPackageEntries(only) { if (!only || only.length === 0) { return Object.entries(PACKAGES).map(([pkgId, pkg]) => [pkgId, pkg, pkgId]); } return only.map(pkgId => { const pkg = getPackageDefinition(pkgId); if (!pkg) { throw new Error(`未知套件: ${pkgId}`); } const canonicalId = PACKAGE_ALIAS_TO_CANONICAL[pkgId] || pkgId; return [pkgId, pkg, canonicalId]; }); } function findSkillSourceDir(skillName) { for (const sourceRoot of SKILL_SOURCE_DIRS) { const candidate = path.join(sourceRoot, skillName); if (fs.existsSync(candidate) && fs.existsSync(path.join(candidate, 'SKILL.md'))) { return candidate; } } return undefined; } function copySkillFiles(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }); for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { if ( entry.name.includes('.bak.') || entry.name.endsWith('~') || entry.name.endsWith('.tmp') ) { continue; } const srcPath = path.join(srcDir, entry.name); const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { copySkillFiles(srcPath, destPath); } else if (entry.isFile()) { fs.copyFileSync(srcPath, destPath); } } } function copyDirRecursive(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }); let copied = 0; for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { const srcPath = path.join(srcDir, entry.name); const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { copied += copyDirRecursive(srcPath, destPath); } else if (entry.isFile()) { fs.copyFileSync(srcPath, destPath); copied++; } } return copied; } // ============================================ // 七牛上传 // ============================================ 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 copyToolPackageFiles(pkgDir) { const toolsDir = path.join(pkgDir, 'tools'); fs.mkdirSync(toolsDir, { recursive: true }); let copied = 0; let missing = 0; for (const file of TOOL_PACKAGE_FILES) { if (!fs.existsSync(file.src)) { missing++; continue; } const dest = path.join(toolsDir, file.dest); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(file.src, dest); copied++; } return { copied, missing }; } // ============================================ // 打包逻辑 // ============================================ function buildPackage(pkgId, pkg, outputPkgId = pkgId) { const pkgDir = path.join(TEMP_BASE, outputPkgId); 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')); } // 复制 set-voc-token.js(Token 设置工具) if (fs.existsSync(SET_VOC_TOKEN_JS)) { fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js')); } // 复制 voc-token-preflight.js(Token 预飞检测工具) if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) { fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js')); } const toolCopyResult = copyToolPackageFiles(pkgDir); let bundleFileCount = 0; if (pkg.bundleDir) { const bundleSrc = path.join(OPENCLAW_SKILLS_ROOT, pkg.bundleDir); if (fs.existsSync(bundleSrc)) { bundleFileCount = copyDirRecursive(bundleSrc, path.join(pkgDir, pkg.bundleDir)); } else { console.log(` ⚠️ 套件目录不存在,跳过: ${pkg.bundleDir}`); } } // 生成 README.txt const readme = [ `OpenClaw ${pkg.label}`, `${'='.repeat(50)}`, '', pkg.description, '', `包含 ${pkg.skills.length} 个技能`, `包含 ${toolCopyResult.copied} 个本地工具文件(安装到 ~/.openclaw/tools 和 ~/.openclaw/workspace/scripts/tools)`, ...(pkg.bundleDir ? [`包含套件入口目录: ${pkg.bundleDir} (${bundleFileCount} 个文件)`] : []), '', '安装方法:', ' 1. 确保已安装 OpenClaw', ' 2. 解压本压缩包', ' 3. 在解压目录运行: node install.js', ' 4. 设置 Token: node ~/.openclaw/tools/set-voc-token.js ', ' 5. 重启 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 # 跳过指定技能', ' node ~/.openclaw/tools/set-voc-token.js # 设置/更新 VOC Token', ' node ~/.openclaw/tools/voc-token-preflight.js # 检测 Token 有效性和余额', '', '技能列表:', ...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 = findSkillSourceDir(skillName); if (!srcDir) { console.log(` ⚠️ 跳过 ${skillName}(未找到源码或已安装目录)`); missing++; continue; } const destDir = path.join(skillsDir, skillName); copySkillFiles(srcDir, destDir); copied++; } console.log(` ✅ 复制 ${copied} 个技能(跳过 ${missing} 个),${toolCopyResult.copied} 个工具文件` + (toolCopyResult.missing > 0 ? `(工具缺失 ${toolCopyResult.missing} 个)` : '') + (pkg.bundleDir ? `,套件入口 ${bundleFileCount} 个文件` : '')); // 压缩 const zipPath = path.join(DIST_DIR, `${outputPkgId}.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; } // ============================================ // Workshop 打包逻辑 // ============================================ function buildWorkshopPackage() { const pkgDir = path.join(TEMP_BASE, 'workshop'); const memoryDir = path.join(pkgDir, 'memory-templates'); const skillsDir = path.join(pkgDir, 'skills'); if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true }); fs.mkdirSync(memoryDir, { recursive: true }); fs.mkdirSync(skillsDir, { recursive: true }); // 复制安装脚本 if (fs.existsSync(INSTALL_WORKSHOP_JS)) { fs.copyFileSync(INSTALL_WORKSHOP_JS, path.join(pkgDir, 'install-workshop.js')); } // 复制 Token 工具(workshop 自足所需) if (fs.existsSync(SET_VOC_TOKEN_JS)) { fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js')); } if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) { fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js')); } const toolCopyResult = copyToolPackageFiles(pkgDir); // 复制凭证模板 if (fs.existsSync(CREDENTIALS_TEMPLATE)) { fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json')); } let copied = 0; // 复制 workshop 文件(仅白名单 playbooks) if (fs.existsSync(WORKSHOP_DIR)) { const mdFiles = fs.readdirSync(WORKSHOP_DIR).filter(f => f.endsWith('.md') && WORKSHOP_MD_WHITELIST.has(f) && fs.statSync(path.join(WORKSHOP_DIR, f)).isFile() ); for (const f of mdFiles) { fs.copyFileSync(path.join(WORKSHOP_DIR, f), path.join(pkgDir, f)); copied++; } } // 复制 memory templates(仅白名单模板) if (fs.existsSync(MEMORY_TEMPLATES_DIR)) { const jsonFiles = fs.readdirSync(MEMORY_TEMPLATES_DIR).filter(f => f.endsWith('.json') && MEMORY_TEMPLATE_WHITELIST.has(f) && fs.statSync(path.join(MEMORY_TEMPLATES_DIR, f)).isFile() ); for (const f of jsonFiles) { fs.copyFileSync(path.join(MEMORY_TEMPLATES_DIR, f), path.join(memoryDir, f)); copied++; } } // 复制 workshop 所需的全部 skills(复用 voc-core 列表,workshop 主线+侧线都依赖它们) const workshopSkills = PACKAGES['voc-core'].skills; let skillsCopied = 0; let skillsMissing = 0; for (const skillName of workshopSkills) { const srcDir = path.join(SKILLS_DIR, skillName); if (!fs.existsSync(srcDir)) { skillsMissing++; 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)); } skillsCopied++; } // 生成 README.txt const readme = [ 'OpenClaw 🦐 VOC虾工作坊 v4.2(自足版)', '==================================================', '', '一个压缩包搞定所有!包含:', ` 🔧 ${skillsCopied} 个 workshop 必需的 skills(品类/评论/竞品/合成/社媒VOC/工作坊)`, ' 📘 11 份 playbook(入口+全局规则+6个Session+配套文档)', ' 📄 9 份 memory templates(记忆模板)', ` 🔑 ${toolCopyResult.copied} 个本地工具文件(Token 工具 + VOC 报告脚本)`, ' 📝 1 份凭证模板(voc-credentials.template.json)', '', '架构:6个Session + 17个Side Quest,主线感知 + 支线深耕', '', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', '📦 一键安装:', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', ' 1. 解压本压缩包', ' 2. 运行: node install-workshop.js', ' (自动安装 skills + playbooks + memory templates + tools + 凭证模板)', ' 3. 设置 Token(首次使用):', ' a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF', ' b. 登录并充值(最低 ¥0.01 体验 1 次)', ' c. 复制 session token(格式 r:xxxx...)', ' d. 运行: node ~/.openclaw/tools/set-voc-token.js ', ' 4. 重启 OpenClaw gateway', ' 5. 对话框说"开始 VOC 工作坊",Agent 会自动进入 Session 0', '', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', '🔍 验证安装:', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', ' node ~/.openclaw/tools/voc-token-preflight.js', ' (期望看到: status=valid, 余额 >= 1)', '', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', '🛠 其他命令:', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', ' node install-workshop.js --dry-run # 预览将要安装的文件', ' node install-workshop.js --skills-only # 只装 skills', ' node install-workshop.js --playbook-only # 只装 playbooks (旧行为)', '', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', '📂 文件结构:', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', ' skills/ ← workshop 用到的 skills', ' voc-token-preflight/ ← Session 0/1 的 Token 预飞闸门', ' brand-context-builder/ ← Session 0 品牌建档', ' category-landscape/ ← Session 1 品类全景', ' keyword-search/, category-tree/, ... (其余 Amazon/VOC/合成 skills)', ' workshop-voc-playbook.md ← 入口路由', ' workshop-global-rules.md ← 全局规则 + Token 预飞协议', ' workshop-session-0-intake.md ← Session 0: 品牌建档 + Token 预飞', ' workshop-session-1-category.md ← Session 1: 品类 + 4 Side Quests', ' workshop-session-2-brand.md ← Session 2: 品牌 + 3 Side Quests', ' workshop-session-3-voc.md ← Session 3: VOC + 3 Side Quests', ' workshop-session-4-deep.md ← Session 4: 深入 + 5 Side Quests', ' workshop-session-5-report.md ← Session 5: 报告 + 2 Side Quests', ' memory-templates/*.json ← 记忆模板(9个)', ' tools/* ← 安装到 ~/.openclaw/tools 和 ~/.openclaw/workspace/scripts/tools', ' set-voc-token.js ← Token 写入工具', ' voc-token-preflight.js ← Token 预飞检测工具', ' voc-credentials.template.json ← 凭证模板', '' ].join('\n'); fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8'); console.log(` ✅ 复制 ${copied} 个 playbook/template + ${skillsCopied} 个 skills + ${toolCopyResult.copied} 个工具文件` + (skillsMissing > 0 ? ` (缺失 ${skillsMissing})` : '') + (toolCopyResult.missing > 0 ? ` (工具缺失 ${toolCopyResult.missing})` : '')); // 压缩 const zipPath = path.join(DIST_DIR, 'workshop.zip'); if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); 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(` 📦 workshop.zip (${(zipSize / 1024).toFixed(0)} KB)`); return zipPath; } // ============================================ // 全量包打包逻辑 // ============================================ function buildAllPackage() { const pkgDir = path.join(TEMP_BASE, 'openclaw-all'); const skillsDir = path.join(pkgDir, 'skills'); const workshopDir = path.join(pkgDir, 'workshop'); const memoryDir = path.join(workshopDir, 'memory-templates'); if (fs.existsSync(pkgDir)) fs.rmSync(pkgDir, { recursive: true, force: true }); fs.mkdirSync(memoryDir, { recursive: true }); // 复制 install-all.js if (fs.existsSync(INSTALL_ALL_JS)) { fs.copyFileSync(INSTALL_ALL_JS, path.join(pkgDir, 'install-all.js')); } // 复制 set-voc-token.js if (fs.existsSync(SET_VOC_TOKEN_JS)) { fs.copyFileSync(SET_VOC_TOKEN_JS, path.join(pkgDir, 'set-voc-token.js')); } // 复制 voc-token-preflight.js if (fs.existsSync(VOC_TOKEN_PREFLIGHT_JS)) { fs.copyFileSync(VOC_TOKEN_PREFLIGHT_JS, path.join(pkgDir, 'voc-token-preflight.js')); } const toolCopyResult = copyToolPackageFiles(pkgDir); // 复制凭证模板 if (fs.existsSync(CREDENTIALS_TEMPLATE)) { fs.copyFileSync(CREDENTIALS_TEMPLATE, path.join(pkgDir, 'voc-credentials.template.json')); } let skillCount = 0; let workshopCount = 0; // 复制发布套件中的技能(排除测试技能和未发布的即梦/视频创作技能) const RELEASED_SKILLS = new Set([ ...PACKAGES['voc-core'].skills, ...PACKAGES['voc-social-data'].skills ]); if (fs.existsSync(SKILLS_DIR)) { const allSkills = fs.readdirSync(SKILLS_DIR).filter(d => { const p = path.join(SKILLS_DIR, d); return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md')) && !SKILL_BLACKLIST.has(d) && RELEASED_SKILLS.has(d); }); for (const skill of allSkills) { const srcDir = path.join(SKILLS_DIR, skill); const destDir = path.join(skillsDir, skill); 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)); } skillCount++; } } console.log(` ✅ 复制 ${skillCount} 个技能 + ${toolCopyResult.copied} 个工具文件` + (toolCopyResult.missing > 0 ? `(工具缺失 ${toolCopyResult.missing} 个)` : '')); // 复制 workshop 文件(仅白名单) if (fs.existsSync(WORKSHOP_DIR)) { const mdFiles = fs.readdirSync(WORKSHOP_DIR).filter(f => f.endsWith('.md') && WORKSHOP_MD_WHITELIST.has(f) && fs.statSync(path.join(WORKSHOP_DIR, f)).isFile() ); for (const f of mdFiles) { fs.copyFileSync(path.join(WORKSHOP_DIR, f), path.join(workshopDir, f)); workshopCount++; } } if (fs.existsSync(MEMORY_TEMPLATES_DIR)) { const jsonFiles = fs.readdirSync(MEMORY_TEMPLATES_DIR).filter(f => f.endsWith('.json') && MEMORY_TEMPLATE_WHITELIST.has(f) && fs.statSync(path.join(MEMORY_TEMPLATES_DIR, f)).isFile() ); for (const f of jsonFiles) { fs.copyFileSync(path.join(MEMORY_TEMPLATES_DIR, f), path.join(memoryDir, f)); workshopCount++; } } console.log(` ✅ 复制 ${workshopCount} 个 workshop 文件`); // README.txt const readme = [ 'OpenClaw 全量安装包 v4.1', '==================================================', '', `包含 ${skillCount} 个技能 + VOC虾工作坊 (${workshopCount}个文件) + ${toolCopyResult.copied} 个本地工具文件`, '', '安装方法:', ' 1. 解压本压缩包', ' 2. 在解压目录运行: node install-all.js', ' 3. 设置 Token:', ' a. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF', ' b. 登录并充值', ' c. 获取 session token (r:xxxx...)', ' d. 运行: node ~/.openclaw/tools/set-voc-token.js ', ' 4. 重启 OpenClaw gateway', '', '其他命令:', ' node install-all.js --list # 列出包含内容', ' node install-all.js --dry-run # 预览模式', ' node install-all.js --skills-only # 只装 Skills', ' node install-all.js --workshop-only # 只装 Workshop', ' node ~/.openclaw/tools/set-voc-token.js # 设置/更新 Token', ' node ~/.openclaw/tools/voc-token-preflight.js # 检测 Token 有效性和余额(Workshop 前置检查)', '' ].join('\n'); fs.writeFileSync(path.join(pkgDir, 'README.txt'), readme, 'utf8'); // 压缩 const zipPath = path.join(DIST_DIR, 'openclaw-all.zip'); if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); 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(` 📦 openclaw-all.zip (${(zipSize / 1024).toFixed(0)} KB)`); return zipPath; } // ============================================ // Main // ============================================ async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { showHelp(); return; } console.log(''); console.log('=================================================='); console.log(' OpenClaw Skills + Workshop 打包 + CDN 上传 v4.1'); console.log('=================================================='); console.log(''); // 前置检查 if (!SKILL_SOURCE_DIRS.some(dir => fs.existsSync(dir))) { console.error(`❌ 未找到可用 Skills 源目录`); console.error(` 已检查: ${SKILL_SOURCE_DIRS.join(', ')}`); 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: 打包 Skills console.log('── Step 1: 打包 Skills ──'); const zipFiles = {}; const selectedPackageEntries = getSelectedPackageEntries(options.only); for (const [pkgId, pkg, canonicalId] of selectedPackageEntries) { console.log(` ${pkg.label} (${pkg.skills.length} 个技能)`); zipFiles[pkgId] = buildPackage(canonicalId, pkg, pkgId); if (options.only.length > 0) continue; for (const alias of pkg.aliases || []) { console.log(` ${pkg.label}(兼容旧名 ${alias},${pkg.skills.length} 个技能)`); zipFiles[alias] = buildPackage(canonicalId, pkg, alias); } } console.log(''); // Step 1b: 打包 Workshop if (!options.skipWorkshop) { console.log('── Step 1b: 打包 Workshop ──'); if (fs.existsSync(WORKSHOP_DIR)) { console.log(' 🦐 VOC虾工作坊'); zipFiles['workshop'] = buildWorkshopPackage(); } else { console.log(' ⚠️ Workshop 目录不存在,跳过'); } console.log(''); } // Step 1c: 打包全量包 if (!options.skipAll) { console.log('── Step 1c: 打包全量包 ──'); console.log(' 📦 openclaw-all (Skills + Workshop + Token工具)'); zipFiles['openclaw-all'] = buildAllPackage(); console.log(''); } if (options.skipUpload) { console.log('── Step 2: 跳过上传 ──'); for (const [pkgId, zipPath] of Object.entries(zipFiles)) { console.log(` ${pkgId}: ${zipPath}`); } if (fs.existsSync(TEMP_BASE)) fs.rmSync(TEMP_BASE, { recursive: true, force: true }); return; } // Step 2: 上传到七牛 console.log('── Step 2: 上传到七牛云 CDN ──'); const downloadUrls = {}; for (const [pkgId, zipPath] of Object.entries(zipFiles)) { const pkg = getPackageDefinition(pkgId); const cdnKey = `${CDN_PREFIX}/${pkgId}.zip`; const label = pkgId === 'workshop' ? '🦐 VOC虾工作坊' : pkgId === 'openclaw-all' ? '📦 全量包' : getPackageLogLabel(pkgId); process.stdout.write(` ${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)) { if (pkgId === 'workshop') { console.log(' 🦐 VOC虾工作坊'); } else if (pkgId === 'openclaw-all') { console.log(' 📦 全量包'); } else { const pkg = getPackageDefinition(pkgId); if (pkg) 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); });