douyin-speaking-daily-suite.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const os = require('os');
  5. const { spawnSync } = require('child_process');
  6. const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
  7. const DIST_DIR = path.join(PROJECT_ROOT, 'dist');
  8. const OPENCLAW_SKILLS_ROOT = path.join(PROJECT_ROOT, 'openclaw-skills');
  9. const SUITE_DIR = path.join(OPENCLAW_SKILLS_ROOT, 'douyin-speaking-daily');
  10. const SUITE_MANIFEST = path.join(SUITE_DIR, 'skill-package-manifest.json');
  11. const PACKAGE_ZIP = path.join(DIST_DIR, 'douyin-speaking-daily.zip');
  12. const DIST_MANIFEST = path.join(DIST_DIR, 'douyin-speaking-daily-suite-manifest.json');
  13. const NODE_CMD = process.execPath || (process.platform === 'win32' ? 'node.exe' : 'node');
  14. const SKILLS = [
  15. 'voc-token-preflight',
  16. 'douyin-general-search',
  17. 'douyin-hashtag-search',
  18. 'douyin-video-detail',
  19. 'douyin-video-comments',
  20. 'douyin-comment-replies',
  21. 'douyin-user-search',
  22. 'douyin-user-profile',
  23. 'douyin-user-posts',
  24. 'douyin-mini-voc',
  25. 'douyin-speaking-profile-builder',
  26. 'douyin-speaking-profile-memory',
  27. 'douyin-speaking-keyword-monitor',
  28. 'douyin-speaking-account-monitor',
  29. 'douyin-speaking-daily-runner',
  30. 'douyin-speaking-daily-report',
  31. 'douyin-viral-script-analyzer',
  32. 'douyin-video-transcript'
  33. ];
  34. const TOOL_FILES = [
  35. 'openclaw-tool-runner.js',
  36. 'set-voc-token.js',
  37. 'voc-token-preflight.js',
  38. 'douyin-speaking-profile-memory.js',
  39. 'douyin-speaking-daily-runner.js',
  40. 'douyin-speaking-daily-report.js',
  41. 'douyin-speaking-daily-p1-smoke.js',
  42. 'douyin-viral-script-analyzer.js',
  43. 'douyin-video-transcriber.js'
  44. ];
  45. const REQUIRED_FILES = [
  46. 'openclaw-skills/douyin-speaking-daily/README.md',
  47. 'openclaw-skills/douyin-speaking-daily/douyin-speaking-daily-playbook.md',
  48. 'openclaw-skills/douyin-speaking-daily/openclaw-startup.md',
  49. 'openclaw-skills/douyin-speaking-daily/skill-package-manifest.json',
  50. 'openclaw-skills/douyin-speaking-daily/docs/specs/douyin-speaking-daily-p1-optimization-spec.md',
  51. 'openclaw-skills/douyin-speaking-daily/memory-templates/douyin-speaking-profile.json',
  52. 'scripts/tools/douyin-speaking-profile-memory.js',
  53. 'scripts/tools/douyin-speaking-daily-runner.js',
  54. 'scripts/tools/douyin-speaking-daily-report.js',
  55. 'scripts/tools/douyin-speaking-daily-p1-smoke.js',
  56. 'scripts/tools/douyin-viral-script-analyzer.js',
  57. 'scripts/tools/douyin-video-transcriber.js'
  58. ];
  59. const SKILL_SOURCE_ROOTS = [
  60. 'voc',
  61. 'social-media',
  62. 'douyin',
  63. 'xiaohongshu',
  64. 'review-analysis',
  65. 'competitor-analysis',
  66. 'synthesis',
  67. 'social-voc',
  68. 'workshop'
  69. ].map(item => path.join(OPENCLAW_SKILLS_ROOT, item));
  70. function parseArgs(argv) {
  71. const opts = {
  72. validate: false,
  73. build: false,
  74. deploy: false,
  75. upload: false,
  76. dryRun: false,
  77. openclawDir: path.join(os.homedir(), '.openclaw'),
  78. help: false
  79. };
  80. for (let i = 0; i < argv.length; i++) {
  81. const token = argv[i];
  82. if (token === '--validate') opts.validate = true;
  83. else if (token === '--build') opts.build = true;
  84. else if (token === '--deploy') opts.deploy = true;
  85. else if (token === '--all') {
  86. opts.validate = true;
  87. opts.build = true;
  88. opts.deploy = true;
  89. } else if (token === '--upload') opts.upload = true;
  90. else if (token === '--dry-run') opts.dryRun = true;
  91. else if (token === '--openclaw-dir') opts.openclawDir = path.resolve(argv[++i] || opts.openclawDir);
  92. else if (token === '--help' || token === '-h') opts.help = true;
  93. }
  94. if (!opts.validate && !opts.build && !opts.deploy && !opts.help) {
  95. opts.validate = true;
  96. opts.build = true;
  97. }
  98. return opts;
  99. }
  100. function usage() {
  101. return [
  102. 'Usage:',
  103. ' node scripts/deploy/douyin-speaking-daily-suite.js',
  104. ' node scripts/deploy/douyin-speaking-daily-suite.js --validate',
  105. ' node scripts/deploy/douyin-speaking-daily-suite.js --build',
  106. ' node scripts/deploy/douyin-speaking-daily-suite.js --build --upload',
  107. ' node scripts/deploy/douyin-speaking-daily-suite.js --deploy --dry-run',
  108. ' node scripts/deploy/douyin-speaking-daily-suite.js --all',
  109. '',
  110. 'Default mode is --validate --build without CDN upload.',
  111. '',
  112. 'Options:',
  113. ' --upload Upload zip to CDN through package-and-upload.js.',
  114. ' --deploy Install suite files into ~/.openclaw.',
  115. ' --openclaw-dir <path> Override OpenClaw root for deploy.',
  116. ' --dry-run Preview deploy writes.',
  117. ' --help, -h Show help.'
  118. ].join('\n');
  119. }
  120. function ensureDir(dir) {
  121. fs.mkdirSync(dir, { recursive: true });
  122. }
  123. function readJson(filePath) {
  124. return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  125. }
  126. function findSkillDir(skillName) {
  127. for (const root of SKILL_SOURCE_ROOTS) {
  128. const candidate = path.join(root, skillName);
  129. if (fs.existsSync(path.join(candidate, 'SKILL.md'))) return candidate;
  130. }
  131. return '';
  132. }
  133. function copyDirRecursive(srcDir, destDir, opts) {
  134. let count = 0;
  135. if (!opts.dryRun) ensureDir(destDir);
  136. for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
  137. const src = path.join(srcDir, entry.name);
  138. const dest = path.join(destDir, entry.name);
  139. if (entry.isDirectory()) {
  140. count += copyDirRecursive(src, dest, opts);
  141. } else if (entry.isFile()) {
  142. if (opts.dryRun) {
  143. console.log(` [DRY] ${src} -> ${dest}`);
  144. } else {
  145. ensureDir(path.dirname(dest));
  146. fs.copyFileSync(src, dest);
  147. }
  148. count++;
  149. }
  150. }
  151. return count;
  152. }
  153. function removeDirInside(targetDir, parentDir, opts) {
  154. const target = path.resolve(targetDir);
  155. const parent = path.resolve(parentDir);
  156. if (!target.startsWith(parent + path.sep)) {
  157. throw new Error(`Refusing to remove path outside target root: ${target}`);
  158. }
  159. if (opts.dryRun) {
  160. console.log(` [DRY] remove ${target}`);
  161. return;
  162. }
  163. fs.rmSync(target, { recursive: true, force: true });
  164. }
  165. function runNodeCheck(filePath) {
  166. const source = fs.readFileSync(filePath, 'utf8').replace(/^#!.*\r?\n/, '');
  167. try {
  168. // Syntax-only check without spawning a nested Node process. This keeps the
  169. // suite validator usable inside restricted runners that block child Node.
  170. new Function('require', 'module', 'exports', '__filename', '__dirname', source);
  171. } catch (error) {
  172. throw new Error(`syntax check failed for ${filePath}: ${error.message}`);
  173. }
  174. }
  175. function validateSuite() {
  176. console.log('[validate] douyin-speaking-daily suite');
  177. const missing = [];
  178. for (const rel of REQUIRED_FILES) {
  179. const file = path.join(PROJECT_ROOT, rel);
  180. if (!fs.existsSync(file)) missing.push(rel);
  181. }
  182. for (const skill of SKILLS) {
  183. const dir = findSkillDir(skill);
  184. if (!dir) {
  185. missing.push(`skill:${skill}`);
  186. continue;
  187. }
  188. const apiConfig = path.join(dir, 'api-config.json');
  189. if (fs.existsSync(apiConfig)) readJson(apiConfig);
  190. }
  191. for (const file of [
  192. 'scripts/tools/douyin-speaking-profile-memory.js',
  193. 'scripts/tools/douyin-speaking-daily-runner.js',
  194. 'scripts/tools/douyin-speaking-daily-report.js',
  195. 'scripts/tools/douyin-speaking-daily-p1-smoke.js',
  196. 'scripts/tools/douyin-viral-script-analyzer.js',
  197. 'scripts/tools/douyin-video-transcriber.js'
  198. ]) {
  199. runNodeCheck(path.join(PROJECT_ROOT, file));
  200. }
  201. readJson(SUITE_MANIFEST);
  202. if (missing.length) {
  203. throw new Error(`Missing suite files:\n${missing.map(item => ` - ${item}`).join('\n')}`);
  204. }
  205. console.log(` ok: ${SKILLS.length} skills, ${TOOL_FILES.length} tools, workspace bundle present`);
  206. }
  207. function buildSuite(opts) {
  208. console.log('[build] dist/douyin-speaking-daily.zip');
  209. ensureDir(DIST_DIR);
  210. const args = [
  211. path.join('scripts', 'deploy', 'package-and-upload.js'),
  212. '--only', 'douyin-speaking-daily',
  213. '--skip-workshop',
  214. '--skip-all'
  215. ];
  216. if (!opts.upload) args.push('--skip-upload');
  217. const child = spawnSync(NODE_CMD, args, {
  218. cwd: PROJECT_ROOT,
  219. encoding: 'utf8',
  220. maxBuffer: 1024 * 1024 * 100
  221. });
  222. if (child.stdout) process.stdout.write(child.stdout);
  223. if (child.stderr) process.stderr.write(child.stderr);
  224. if (child.status !== 0) {
  225. const details = child.error ? ` (${child.error.code || child.error.name}: ${child.error.message})` : '';
  226. throw new Error(`package-and-upload failed with exit ${child.status}${details}`);
  227. }
  228. if (!fs.existsSync(PACKAGE_ZIP)) {
  229. throw new Error(`Expected package was not generated: ${PACKAGE_ZIP}`);
  230. }
  231. writeDistManifest({ upload: opts.upload });
  232. }
  233. function deploySuite(opts) {
  234. const openclawDir = path.resolve(opts.openclawDir);
  235. const skillsTarget = path.join(openclawDir, 'skills');
  236. const toolsTarget = path.join(openclawDir, 'tools');
  237. const workspaceTarget = path.join(openclawDir, 'workspace');
  238. const workspaceToolsTarget = path.join(workspaceTarget, 'scripts', 'tools');
  239. console.log(`[deploy] ${openclawDir}${opts.dryRun ? ' (dry run)' : ''}`);
  240. if (!opts.dryRun && !fs.existsSync(openclawDir)) {
  241. throw new Error(`OpenClaw directory not found: ${openclawDir}`);
  242. }
  243. for (const skill of SKILLS) {
  244. const srcDir = findSkillDir(skill);
  245. if (!srcDir) throw new Error(`Cannot deploy missing skill: ${skill}`);
  246. const destDir = path.join(skillsTarget, skill);
  247. removeDirInside(destDir, skillsTarget, opts);
  248. copyDirRecursive(srcDir, destDir, opts);
  249. console.log(` [skill] ${skill}`);
  250. }
  251. for (const tool of TOOL_FILES) {
  252. const src = path.join(PROJECT_ROOT, 'scripts', 'tools', tool);
  253. if (!fs.existsSync(src)) throw new Error(`Missing tool: ${src}`);
  254. for (const targetRoot of [toolsTarget, workspaceToolsTarget]) {
  255. const dest = path.join(targetRoot, tool);
  256. if (opts.dryRun) {
  257. console.log(` [DRY] ${src} -> ${dest}`);
  258. } else {
  259. ensureDir(path.dirname(dest));
  260. fs.copyFileSync(src, dest);
  261. }
  262. }
  263. console.log(` [tool] ${tool}`);
  264. }
  265. const bundleDest = path.join(workspaceTarget, 'douyin-speaking-daily');
  266. removeDirInside(bundleDest, workspaceTarget, opts);
  267. copyDirRecursive(SUITE_DIR, bundleDest, opts);
  268. console.log(' [workspace] douyin-speaking-daily');
  269. }
  270. function writeDistManifest(extra = {}) {
  271. const packageMeta = readJson(SUITE_MANIFEST);
  272. const zipStat = fs.existsSync(PACKAGE_ZIP) ? fs.statSync(PACKAGE_ZIP) : undefined;
  273. const manifest = {
  274. name: 'douyin-speaking-daily',
  275. version: packageMeta.version,
  276. generatedAt: new Date().toISOString(),
  277. packageZip: fs.existsSync(PACKAGE_ZIP) ? path.relative(PROJECT_ROOT, PACKAGE_ZIP).replace(/\\/g, '/') : '',
  278. packageZipBytes: zipStat ? zipStat.size : 0,
  279. skills: SKILLS,
  280. tools: TOOL_FILES,
  281. workspaceBundle: 'douyin-speaking-daily',
  282. installCommand: 'node install.js',
  283. localDeployCommand: 'node scripts/deploy/douyin-speaking-daily-suite.js --deploy',
  284. uploadCommand: 'node scripts/deploy/douyin-speaking-daily-suite.js --build --upload',
  285. ...extra
  286. };
  287. fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
  288. console.log(`[manifest] ${path.relative(PROJECT_ROOT, DIST_MANIFEST)}`);
  289. }
  290. function main() {
  291. const opts = parseArgs(process.argv.slice(2));
  292. if (opts.help) {
  293. console.log(usage());
  294. return;
  295. }
  296. try {
  297. if (opts.validate) validateSuite();
  298. if (opts.build) buildSuite(opts);
  299. if (opts.deploy) deploySuite(opts);
  300. if (!opts.build) writeDistManifest({ upload: opts.upload });
  301. console.log('[done] douyin-speaking-daily suite is ready');
  302. } catch (error) {
  303. console.error(`[error] ${error.message}`);
  304. process.exit(1);
  305. }
  306. }
  307. main();