douyin-speaking-daily-suite.js 11 KB

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