install-workshop.js 13 KB

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