install-workshop.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. #!/usr/bin/env node
  2. /**
  3. * VOC 虾工作坊 一键安装脚本 v4.2
  4. *
  5. * 将 workshop.zip 解压后的全部组件(skills + playbooks + memory templates + tools + credentials)
  6. * 安装到 OpenClaw 对应目录。
  7. *
  8. * 用法:
  9. * node install-workshop.js # 安装全部(默认)
  10. * node install-workshop.js --dry-run # 预览模式
  11. * node install-workshop.js --skills-only # 只装 skills
  12. * node install-workshop.js --playbook-only # 只装 playbooks(旧行为,兼容)
  13. * node install-workshop.js --help # 帮助
  14. */
  15. const fs = require('fs');
  16. const os = require('os');
  17. const path = require('path');
  18. // ============================================
  19. // 参数
  20. // ============================================
  21. const args = process.argv.slice(2);
  22. const DRY_RUN = args.includes('--dry-run');
  23. const SKILLS_ONLY = args.includes('--skills-only');
  24. const PLAYBOOK_ONLY = args.includes('--playbook-only');
  25. const HELP = args.includes('--help') || args.includes('-h');
  26. // ============================================
  27. // 路径
  28. // ============================================
  29. const SRC_DIR = __dirname;
  30. const HOME = os.homedir();
  31. const OPENCLAW_DIR = path.join(HOME, '.openclaw');
  32. const WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
  33. const MEMORY_DIR = path.join(WORKSPACE_DIR, 'memory');
  34. const SKILLS_TARGET = path.join(OPENCLAW_DIR, 'skills');
  35. const TOOLS_TARGET = path.join(OPENCLAW_DIR, 'tools');
  36. const CREDENTIALS_PATH = path.join(OPENCLAW_DIR, 'voc-credentials.json');
  37. // ============================================
  38. // 工具函数
  39. // ============================================
  40. function ensureDir(dir) {
  41. if (!DRY_RUN && !fs.existsSync(dir)) {
  42. fs.mkdirSync(dir, { recursive: true });
  43. }
  44. }
  45. function copyFile(src, dest) {
  46. if (DRY_RUN) return;
  47. ensureDir(path.dirname(dest));
  48. fs.copyFileSync(src, dest);
  49. }
  50. function formatSize(bytes) {
  51. if (bytes < 1024) return bytes + ' B';
  52. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
  53. return (bytes / 1024 / 1024).toFixed(1) + ' MB';
  54. }
  55. // ============================================
  56. // Help
  57. // ============================================
  58. function showHelp() {
  59. console.log(`
  60. 🦐 VOC 虾工作坊 安装脚本 v4.2
  61. Usage:
  62. node install-workshop.js [options]
  63. Options:
  64. --dry-run 预览模式(不实际复制)
  65. --skills-only 只装 skills
  66. --playbook-only 只装 playbooks + memory templates(旧行为)
  67. --help, -h 显示帮助
  68. 默认(不加参数)安装全部:
  69. skills/* → ~/.openclaw/skills/
  70. *.md → ~/.openclaw/workspace/
  71. memory-templates/* → ~/.openclaw/workspace/memory/
  72. set-voc-token.js → ~/.openclaw/tools/
  73. voc-token-preflight.js → ~/.openclaw/tools/
  74. voc-credentials.template.json → ~/.openclaw/voc-credentials.json(若不存在)
  75. Token 设置流程:
  76. 1. 打开 https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF
  77. 2. 登录并充值
  78. 3. 复制 session token (格式: r:xxxx...)
  79. 4. 运行: node ~/.openclaw/tools/set-voc-token.js <your-token>
  80. 5. 重启 OpenClaw gateway
  81. `);
  82. }
  83. // ============================================
  84. // Skills 安装
  85. // ============================================
  86. function installSkills() {
  87. const skillsSrcDir = path.join(SRC_DIR, 'skills');
  88. if (!fs.existsSync(skillsSrcDir)) {
  89. console.log(' ⚠️ skills/ 目录不存在,跳过');
  90. return 0;
  91. }
  92. const skills = fs.readdirSync(skillsSrcDir).filter(d => {
  93. const p = path.join(skillsSrcDir, d);
  94. return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
  95. }).sort();
  96. console.log(`── Skills (${skills.length}) → ${SKILLS_TARGET} ──`);
  97. for (const skill of skills) {
  98. const srcSkillDir = path.join(skillsSrcDir, skill);
  99. const destSkillDir = path.join(SKILLS_TARGET, skill);
  100. const files = fs.readdirSync(srcSkillDir).filter(f =>
  101. fs.statSync(path.join(srcSkillDir, f)).isFile()
  102. );
  103. // 覆盖式安装(先删除目标)
  104. if (!DRY_RUN && fs.existsSync(destSkillDir)) {
  105. fs.rmSync(destSkillDir, { recursive: true, force: true });
  106. }
  107. ensureDir(destSkillDir);
  108. for (const f of files) {
  109. copyFile(path.join(srcSkillDir, f), path.join(destSkillDir, f));
  110. }
  111. if (DRY_RUN) {
  112. console.log(` 📄 ${skill} (${files.length} files)`);
  113. } else {
  114. console.log(` ✅ ${skill}`);
  115. }
  116. }
  117. console.log('');
  118. return skills.length;
  119. }
  120. // ============================================
  121. // Playbooks + Memory Templates 安装
  122. // ============================================
  123. function installPlaybooks() {
  124. let count = 0;
  125. // .md 文件 → workspace
  126. const mdFiles = fs.readdirSync(SRC_DIR).filter(f =>
  127. f.endsWith('.md') && fs.statSync(path.join(SRC_DIR, f)).isFile()
  128. );
  129. if (mdFiles.length > 0) {
  130. console.log(`── Playbooks (${mdFiles.length}) → ${WORKSPACE_DIR} ──`);
  131. for (const f of mdFiles) {
  132. const src = path.join(SRC_DIR, f);
  133. const dest = path.join(WORKSPACE_DIR, f);
  134. const size = formatSize(fs.statSync(src).size);
  135. copyFile(src, dest);
  136. console.log(DRY_RUN ? ` 📄 ${f} (${size})` : ` ✅ ${f} (${size})`);
  137. count++;
  138. }
  139. console.log('');
  140. }
  141. // memory-templates → workspace/memory
  142. const memSrcDir = path.join(SRC_DIR, 'memory-templates');
  143. if (fs.existsSync(memSrcDir)) {
  144. const jsonFiles = fs.readdirSync(memSrcDir).filter(f =>
  145. f.endsWith('.json') && fs.statSync(path.join(memSrcDir, f)).isFile()
  146. );
  147. if (jsonFiles.length > 0) {
  148. console.log(`── Memory Templates (${jsonFiles.length}) → ${MEMORY_DIR} ──`);
  149. for (const f of jsonFiles) {
  150. const src = path.join(memSrcDir, f);
  151. const dest = path.join(MEMORY_DIR, f);
  152. const size = formatSize(fs.statSync(src).size);
  153. copyFile(src, dest);
  154. console.log(DRY_RUN ? ` 📄 ${f} (${size})` : ` ✅ ${f} (${size})`);
  155. count++;
  156. }
  157. console.log('');
  158. }
  159. }
  160. return count;
  161. }
  162. // ============================================
  163. // Tools 安装
  164. // ============================================
  165. function installTools() {
  166. const tools = [
  167. { name: 'set-voc-token.js', desc: 'Token 写入工具' },
  168. { name: 'voc-token-preflight.js', desc: 'Token 预飞检测工具(Workshop 前置检查)' }
  169. ];
  170. const available = tools.filter(t => fs.existsSync(path.join(SRC_DIR, t.name)));
  171. if (available.length === 0) return 0;
  172. console.log(`── Tools (${available.length}) → ${TOOLS_TARGET} ──`);
  173. for (const { name, desc } of available) {
  174. const src = path.join(SRC_DIR, name);
  175. const dest = path.join(TOOLS_TARGET, name);
  176. copyFile(src, dest);
  177. console.log(DRY_RUN
  178. ? ` 📄 ${name} → ${dest} (${desc})`
  179. : ` ✅ ${name} (${desc})`);
  180. }
  181. console.log('');
  182. return available.length;
  183. }
  184. // ============================================
  185. // Credentials Template 安装(不覆盖已有凭证)
  186. // ============================================
  187. function installCredentialsTemplate() {
  188. const templatePath = path.join(SRC_DIR, 'voc-credentials.template.json');
  189. if (!fs.existsSync(templatePath)) return false;
  190. console.log(`── Credentials Template → ${OPENCLAW_DIR} ──`);
  191. // 总是安装 template 本身(作参考)
  192. const templateDest = path.join(OPENCLAW_DIR, 'voc-credentials.template.json');
  193. copyFile(templatePath, templateDest);
  194. console.log(DRY_RUN
  195. ? ` 📄 voc-credentials.template.json → ${templateDest}`
  196. : ` ✅ voc-credentials.template.json (参考模板)`);
  197. // 只有当 voc-credentials.json 不存在时才拷贝为工作配置
  198. if (fs.existsSync(CREDENTIALS_PATH)) {
  199. console.log(` ⏭️ voc-credentials.json 已存在,保留用户配置(未覆盖)`);
  200. } else {
  201. copyFile(templatePath, CREDENTIALS_PATH);
  202. console.log(DRY_RUN
  203. ? ` 📄 voc-credentials.json → ${CREDENTIALS_PATH} (新建)`
  204. : ` ✅ voc-credentials.json (新建,待填写 vocToken)`);
  205. }
  206. console.log('');
  207. return true;
  208. }
  209. // ============================================
  210. // Main
  211. // ============================================
  212. function main() {
  213. if (HELP) { showHelp(); return; }
  214. console.log('');
  215. console.log('══════════════════════════════════════════════════');
  216. console.log(' 🦐 VOC 虾工作坊 安装脚本 v4.2(自足版)');
  217. console.log('══════════════════════════════════════════════════');
  218. if (DRY_RUN) console.log(' 📌 预览模式(不会实际复制)');
  219. if (SKILLS_ONLY) console.log(' 📌 只装 skills');
  220. if (PLAYBOOK_ONLY) console.log(' 📌 只装 playbooks + memory templates');
  221. console.log('');
  222. // 确保目标根目录
  223. ensureDir(OPENCLAW_DIR);
  224. ensureDir(WORKSPACE_DIR);
  225. ensureDir(MEMORY_DIR);
  226. ensureDir(SKILLS_TARGET);
  227. ensureDir(TOOLS_TARGET);
  228. let skillsCount = 0;
  229. let playbookCount = 0;
  230. let toolsCount = 0;
  231. let credInstalled = false;
  232. // Skills
  233. if (!PLAYBOOK_ONLY) {
  234. skillsCount = installSkills();
  235. }
  236. // Playbooks + Memory Templates
  237. if (!SKILLS_ONLY) {
  238. playbookCount = installPlaybooks();
  239. }
  240. // Tools + Credentials(总是安装,除非 --playbook-only)
  241. if (!PLAYBOOK_ONLY && !SKILLS_ONLY) {
  242. toolsCount = installTools();
  243. credInstalled = installCredentialsTemplate();
  244. } else if (SKILLS_ONLY) {
  245. // skills-only 也带上 tools,因为 Session 0 Step 0.4 的预飞依赖它
  246. toolsCount = installTools();
  247. }
  248. // Summary
  249. console.log('══════════════════════════════════════════════════');
  250. const prefix = DRY_RUN ? '[预览] ' : '';
  251. console.log(`${prefix}✅ 安装完成`);
  252. if (!PLAYBOOK_ONLY) console.log(` 🔧 Skills: ${skillsCount} 个 → ${SKILLS_TARGET}`);
  253. if (!SKILLS_ONLY) console.log(` � Playbooks + Memory: ${playbookCount} 个 → ${WORKSPACE_DIR}`);
  254. if (toolsCount > 0) console.log(` 🔑 Tools: ${toolsCount} 个 → ${TOOLS_TARGET}`);
  255. if (credInstalled) console.log(` 📝 Credentials: ${CREDENTIALS_PATH}`);
  256. console.log('');
  257. if (!DRY_RUN) {
  258. console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  259. console.log('� 下一步:设置 Token(首次使用)');
  260. console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  261. console.log(' 1. 打开充值页面(扫码登录 + 充值):');
  262. console.log(' https://app.fmode.cn/dev/apig-pay/?apigid=7HwdQZk55B&fun_id=HOkkX72PMF');
  263. console.log('');
  264. console.log(' 2. 复制 session token(格式: r:xxxx...)');
  265. console.log('');
  266. console.log(' 3. 运行:');
  267. console.log(' node ~/.openclaw/tools/set-voc-token.js <your-token>');
  268. console.log('');
  269. console.log(' 4. 验证(可选):');
  270. console.log(' node ~/.openclaw/tools/voc-token-preflight.js');
  271. console.log(' 期望看到: status=valid, 余额 >= 1');
  272. console.log('');
  273. console.log(' 5. 重启 OpenClaw gateway');
  274. console.log('');
  275. console.log(' 6. 对话框说"开始 VOC 工作坊",Agent 会进入 Session 0');
  276. console.log('');
  277. }
  278. }
  279. main();