#!/usr/bin/env node /** * VOC 虾工作坊 一键安装脚本 v4.2 * * 将 workshop.zip 解压后的全部组件(skills + playbooks + memory templates + tools + credentials) * 安装到 OpenClaw 对应目录。 * * 用法: * node install-workshop.js # 安装全部(默认) * node install-workshop.js --dry-run # 预览模式 * node install-workshop.js --skills-only # 只装 skills * node install-workshop.js --playbook-only # 只装 playbooks(旧行为,兼容) * node install-workshop.js --help # 帮助 */ const fs = require('fs'); const os = require('os'); const path = require('path'); // ============================================ // 参数 // ============================================ const args = process.argv.slice(2); const DRY_RUN = args.includes('--dry-run'); const SKILLS_ONLY = args.includes('--skills-only'); const PLAYBOOK_ONLY = args.includes('--playbook-only'); const HELP = args.includes('--help') || args.includes('-h'); // ============================================ // 路径 // ============================================ const SRC_DIR = __dirname; const HOME = os.homedir(); const OPENCLAW_DIR = path.join(HOME, '.openclaw'); const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace'); const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory'); const SKILLS_TARGET = path.join(OPENCLAW_DIR, 'skills'); const TOOLS_TARGET = path.join(OPENCLAW_DIR, 'tools'); const WORKSPACE_TOOLS_TARGET = path.join(WORKSPACE_DIR, 'scripts', 'tools'); const CREDENTIALS_PATH = path.join(OPENCLAW_DIR, 'voc-credentials.json'); // ============================================ // 工具函数 // ============================================ function ensureDir(dir) { if (!DRY_RUN && !fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function copyFile(src, dest) { if (DRY_RUN) return; ensureDir(path.dirname(dest)); fs.copyFileSync(src, dest); } function copyDirRecursive(srcDir, destDir) { let count = 0; if (!fs.existsSync(srcDir)) return count; for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { const src = path.join(srcDir, entry.name); const dest = path.join(destDir, entry.name); if (entry.isDirectory()) { count += copyDirRecursive(src, dest); } else if (entry.isFile()) { copyFile(src, dest); count++; } } return count; } function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1024 / 1024).toFixed(1) + ' MB'; } // ============================================ // Help // ============================================ function showHelp() { console.log(` 🦐 VOC 虾工作坊 安装脚本 v4.2 Usage: node install-workshop.js [options] Options: --dry-run 预览模式(不实际复制) --skills-only 只装 skills --playbook-only 只装 playbooks + memory templates(旧行为) --help, -h 显示帮助 默认(不加参数)安装全部: skills/* → ~/.openclaw/skills/ *.md → ~/.openclaw/workspace/ memory-templates/* → ~/.openclaw/workspace/memory/ tools/* → ~/.openclaw/tools/ tools/* → ~/.openclaw/workspace/scripts/tools/ voc-credentials.template.json → ~/.openclaw/voc-credentials.json(若不存在) Token 设置流程: 1. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF 2. 登录并充值 3. 复制 session token (格式: r:xxxx...) 4. 运行: node ~/.openclaw/tools/set-voc-token.js 5. 重启 OpenClaw gateway `); } // ============================================ // Skills 安装 // ============================================ function installSkills() { const skillsSrcDir = path.join(SRC_DIR, 'skills'); if (!fs.existsSync(skillsSrcDir)) { console.log(' ⚠️ skills/ 目录不存在,跳过'); return 0; } const skills = fs.readdirSync(skillsSrcDir).filter(d => { const p = path.join(skillsSrcDir, d); return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md')); }).sort(); console.log(`── Skills (${skills.length}) → ${SKILLS_TARGET} ──`); for (const skill of skills) { const srcSkillDir = path.join(skillsSrcDir, skill); const destSkillDir = path.join(SKILLS_TARGET, skill); const files = fs.readdirSync(srcSkillDir).filter(f => fs.statSync(path.join(srcSkillDir, f)).isFile() ); // 覆盖式安装(先删除目标) if (!DRY_RUN && fs.existsSync(destSkillDir)) { fs.rmSync(destSkillDir, { recursive: true, force: true }); } ensureDir(destSkillDir); for (const f of files) { copyFile(path.join(srcSkillDir, f), path.join(destSkillDir, f)); } if (DRY_RUN) { console.log(` 📄 ${skill} (${files.length} files)`); } else { console.log(` ✅ ${skill}`); } } console.log(''); return skills.length; } // ============================================ // Playbooks + Memory Templates 安装 // ============================================ function installPlaybooks() { let count = 0; // .md 文件 → workspace const mdFiles = fs.readdirSync(SRC_DIR).filter(f => f.endsWith('.md') && fs.statSync(path.join(SRC_DIR, f)).isFile() ); if (mdFiles.length > 0) { console.log(`── Playbooks (${mdFiles.length}) → ${WORKSPACE_DIR} ──`); for (const f of mdFiles) { const src = path.join(SRC_DIR, f); const dest = path.join(WORKSPACE_DIR, f); const size = formatSize(fs.statSync(src).size); copyFile(src, dest); console.log(DRY_RUN ? ` 📄 ${f} (${size})` : ` ✅ ${f} (${size})`); count++; } console.log(''); } // memory-templates → workspace/memory const memSrcDir = path.join(SRC_DIR, 'memory-templates'); if (fs.existsSync(memSrcDir)) { const jsonFiles = fs.readdirSync(memSrcDir).filter(f => f.endsWith('.json') && fs.statSync(path.join(memSrcDir, f)).isFile() ); if (jsonFiles.length > 0) { console.log(`── Memory Templates (${jsonFiles.length}) → ${MEMORY_DIR} ──`); for (const f of jsonFiles) { const src = path.join(memSrcDir, f); const dest = path.join(MEMORY_DIR, f); const size = formatSize(fs.statSync(src).size); copyFile(src, dest); console.log(DRY_RUN ? ` 📄 ${f} (${size})` : ` ✅ ${f} (${size})`); count++; } console.log(''); } } return count; } // ============================================ // Tools 安装 // ============================================ function installTools() { let count = 0; const packagedToolsDir = path.join(SRC_DIR, 'tools'); if (fs.existsSync(packagedToolsDir)) { console.log(`── Tools → ${TOOLS_TARGET} ──`); count += copyDirRecursive(packagedToolsDir, TOOLS_TARGET); console.log(DRY_RUN ? ` 📄 tools/* → ${TOOLS_TARGET}` : ` ✅ ${count} files`); console.log(`── Workspace Tool Mirror → ${WORKSPACE_TOOLS_TARGET} ──`); const mirrorCount = copyDirRecursive(packagedToolsDir, WORKSPACE_TOOLS_TARGET); console.log(DRY_RUN ? ` 📄 tools/* → ${WORKSPACE_TOOLS_TARGET}` : ` ✅ ${mirrorCount} files`); console.log(''); return count; } const legacyTools = ['set-voc-token.js', 'voc-token-preflight.js']; const available = legacyTools.filter(name => fs.existsSync(path.join(SRC_DIR, name))); if (available.length === 0) return 0; console.log(`── Tools (${available.length}) → ${TOOLS_TARGET} ──`); for (const name of available) { const src = path.join(SRC_DIR, name); copyFile(src, path.join(TOOLS_TARGET, name)); copyFile(src, path.join(WORKSPACE_TOOLS_TARGET, name)); console.log(DRY_RUN ? ` 📄 ${name}` : ` ✅ ${name}`); count++; } console.log(''); return count; } // ============================================ // Credentials Template 安装(不覆盖已有凭证) // ============================================ function installCredentialsTemplate() { const templatePath = path.join(SRC_DIR, 'voc-credentials.template.json'); if (!fs.existsSync(templatePath)) return false; console.log(`── Credentials Template → ${OPENCLAW_DIR} ──`); // 总是安装 template 本身(作参考) const templateDest = path.join(OPENCLAW_DIR, 'voc-credentials.template.json'); copyFile(templatePath, templateDest); console.log(DRY_RUN ? ` 📄 voc-credentials.template.json → ${templateDest}` : ` ✅ voc-credentials.template.json (参考模板)`); // 只有当 voc-credentials.json 不存在时才拷贝为工作配置 if (fs.existsSync(CREDENTIALS_PATH)) { console.log(` ⏭️ voc-credentials.json 已存在,保留用户配置(未覆盖)`); } else { copyFile(templatePath, CREDENTIALS_PATH); console.log(DRY_RUN ? ` 📄 voc-credentials.json → ${CREDENTIALS_PATH} (新建)` : ` ✅ voc-credentials.json (新建,待填写 vocToken)`); } console.log(''); return true; } // ============================================ // Main // ============================================ function main() { if (HELP) { showHelp(); return; } console.log(''); console.log('══════════════════════════════════════════════════'); console.log(' 🦐 VOC 虾工作坊 安装脚本 v4.2(自足版)'); console.log('══════════════════════════════════════════════════'); if (DRY_RUN) console.log(' 📌 预览模式(不会实际复制)'); if (SKILLS_ONLY) console.log(' 📌 只装 skills'); if (PLAYBOOK_ONLY) console.log(' 📌 只装 playbooks + memory templates'); console.log(''); // 确保目标根目录 ensureDir(OPENCLAW_DIR); ensureDir(WORKSPACE_DIR); ensureDir(MEMORY_DIR); ensureDir(SKILLS_TARGET); ensureDir(TOOLS_TARGET); let skillsCount = 0; let playbookCount = 0; let toolsCount = 0; let credInstalled = false; // Skills if (!PLAYBOOK_ONLY) { skillsCount = installSkills(); } // Playbooks + Memory Templates if (!SKILLS_ONLY) { playbookCount = installPlaybooks(); } // Tools + Credentials(总是安装,除非 --playbook-only) if (!PLAYBOOK_ONLY && !SKILLS_ONLY) { toolsCount = installTools(); credInstalled = installCredentialsTemplate(); } else if (SKILLS_ONLY) { // skills-only 也带上 tools,因为 Session 0 Step 0.4 的预飞依赖它 toolsCount = installTools(); } // Summary console.log('══════════════════════════════════════════════════'); const prefix = DRY_RUN ? '[预览] ' : ''; console.log(`${prefix}✅ 安装完成`); if (!PLAYBOOK_ONLY) console.log(` 🔧 Skills: ${skillsCount} 个 → ${SKILLS_TARGET}`); if (!SKILLS_ONLY) console.log(` � Playbooks + Memory: ${playbookCount} 个 → ${WORKSPACE_DIR}`); if (toolsCount > 0) console.log(` 🔑 Tools: ${toolsCount} 个 → ${TOOLS_TARGET}`); if (credInstalled) console.log(` 📝 Credentials: ${CREDENTIALS_PATH}`); console.log(''); if (!DRY_RUN) { console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('� 下一步:设置 Token(首次使用)'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(' 1. 打开充值页面(扫码登录 + 充值):'); console.log(' https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF'); console.log(''); console.log(' 2. 复制 session token(格式: r:xxxx...)'); console.log(''); console.log(' 3. 运行:'); console.log(' node ~/.openclaw/tools/set-voc-token.js '); console.log(''); console.log(' 4. 验证(可选):'); console.log(' node ~/.openclaw/tools/voc-token-preflight.js'); console.log(' 期望看到: status=valid, 余额 >= 1'); console.log(''); console.log(' 5. 重启 OpenClaw gateway'); console.log(''); console.log(' 6. 对话框说"开始 VOC 工作坊",Agent 会进入 Session 0'); console.log(''); } } main();