| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const os = require('os');
- const { spawnSync } = require('child_process');
- const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
- const DIST_DIR = path.join(PROJECT_ROOT, 'dist');
- const OPENCLAW_SKILLS_ROOT = path.join(PROJECT_ROOT, 'openclaw-skills');
- const SUITE_DIR = path.join(OPENCLAW_SKILLS_ROOT, 'douyin-speaking-daily');
- const SUITE_MANIFEST = path.join(SUITE_DIR, 'skill-package-manifest.json');
- const PACKAGE_ZIP = path.join(DIST_DIR, 'douyin-speaking-daily.zip');
- const DIST_MANIFEST = path.join(DIST_DIR, 'douyin-speaking-daily-suite-manifest.json');
- const NODE_CMD = process.execPath || (process.platform === 'win32' ? 'node.exe' : 'node');
- const SKILLS = [
- 'voc-token-preflight',
- 'douyin-general-search',
- 'douyin-hashtag-search',
- 'douyin-video-detail',
- 'douyin-video-comments',
- 'douyin-comment-replies',
- 'douyin-user-search',
- 'douyin-user-profile',
- 'douyin-user-posts',
- 'douyin-mini-voc',
- 'douyin-speaking-profile-builder',
- 'douyin-speaking-profile-memory',
- 'douyin-speaking-keyword-monitor',
- 'douyin-speaking-account-monitor',
- 'douyin-speaking-daily-runner',
- 'douyin-speaking-daily-report',
- 'douyin-viral-script-analyzer',
- 'douyin-video-transcript'
- ];
- const TOOL_FILES = [
- 'openclaw-tool-runner.js',
- 'set-voc-token.js',
- 'voc-token-preflight.js',
- 'douyin-speaking-profile-memory.js',
- 'douyin-speaking-daily-runner.js',
- 'douyin-speaking-daily-report.js',
- 'douyin-speaking-daily-p1-smoke.js',
- 'douyin-viral-script-analyzer.js',
- 'douyin-video-transcriber.js'
- ];
- const REQUIRED_FILES = [
- 'openclaw-skills/douyin-speaking-daily/README.md',
- 'openclaw-skills/douyin-speaking-daily/douyin-speaking-daily-playbook.md',
- 'openclaw-skills/douyin-speaking-daily/openclaw-startup.md',
- 'openclaw-skills/douyin-speaking-daily/skill-package-manifest.json',
- 'openclaw-skills/douyin-speaking-daily/docs/specs/douyin-speaking-daily-p1-optimization-spec.md',
- 'openclaw-skills/douyin-speaking-daily/memory-templates/douyin-speaking-profile.json',
- 'scripts/tools/douyin-speaking-profile-memory.js',
- 'scripts/tools/douyin-speaking-daily-runner.js',
- 'scripts/tools/douyin-speaking-daily-report.js',
- 'scripts/tools/douyin-speaking-daily-p1-smoke.js',
- 'scripts/tools/douyin-viral-script-analyzer.js',
- 'scripts/tools/douyin-video-transcriber.js'
- ];
- const SKILL_SOURCE_ROOTS = [
- 'voc',
- 'social-media',
- 'douyin',
- 'xiaohongshu',
- 'review-analysis',
- 'competitor-analysis',
- 'synthesis',
- 'social-voc',
- 'workshop'
- ].map(item => path.join(OPENCLAW_SKILLS_ROOT, item));
- function parseArgs(argv) {
- const opts = {
- validate: false,
- build: false,
- deploy: false,
- upload: false,
- dryRun: false,
- openclawDir: path.join(os.homedir(), '.openclaw'),
- help: false
- };
- for (let i = 0; i < argv.length; i++) {
- const token = argv[i];
- if (token === '--validate') opts.validate = true;
- else if (token === '--build') opts.build = true;
- else if (token === '--deploy') opts.deploy = true;
- else if (token === '--all') {
- opts.validate = true;
- opts.build = true;
- opts.deploy = true;
- } else if (token === '--upload') opts.upload = true;
- else if (token === '--dry-run') opts.dryRun = true;
- else if (token === '--openclaw-dir') opts.openclawDir = path.resolve(argv[++i] || opts.openclawDir);
- else if (token === '--help' || token === '-h') opts.help = true;
- }
- if (!opts.validate && !opts.build && !opts.deploy && !opts.help) {
- opts.validate = true;
- opts.build = true;
- }
- return opts;
- }
- function usage() {
- return [
- 'Usage:',
- ' node scripts/deploy/douyin-speaking-daily-suite.js',
- ' node scripts/deploy/douyin-speaking-daily-suite.js --validate',
- ' node scripts/deploy/douyin-speaking-daily-suite.js --build',
- ' node scripts/deploy/douyin-speaking-daily-suite.js --build --upload',
- ' node scripts/deploy/douyin-speaking-daily-suite.js --deploy --dry-run',
- ' node scripts/deploy/douyin-speaking-daily-suite.js --all',
- '',
- 'Default mode is --validate --build without CDN upload.',
- '',
- 'Options:',
- ' --upload Upload zip to CDN through package-and-upload.js.',
- ' --deploy Install suite files into ~/.openclaw.',
- ' --openclaw-dir <path> Override OpenClaw root for deploy.',
- ' --dry-run Preview deploy writes.',
- ' --help, -h Show help.'
- ].join('\n');
- }
- function ensureDir(dir) {
- fs.mkdirSync(dir, { recursive: true });
- }
- function readJson(filePath) {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- }
- function findSkillDir(skillName) {
- for (const root of SKILL_SOURCE_ROOTS) {
- const candidate = path.join(root, skillName);
- if (fs.existsSync(path.join(candidate, 'SKILL.md'))) return candidate;
- }
- return '';
- }
- function copyDirRecursive(srcDir, destDir, opts) {
- let count = 0;
- if (!opts.dryRun) ensureDir(destDir);
- 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, opts);
- } else if (entry.isFile()) {
- if (opts.dryRun) {
- console.log(` [DRY] ${src} -> ${dest}`);
- } else {
- ensureDir(path.dirname(dest));
- fs.copyFileSync(src, dest);
- }
- count++;
- }
- }
- return count;
- }
- function removeDirInside(targetDir, parentDir, opts) {
- const target = path.resolve(targetDir);
- const parent = path.resolve(parentDir);
- if (!target.startsWith(parent + path.sep)) {
- throw new Error(`Refusing to remove path outside target root: ${target}`);
- }
- if (opts.dryRun) {
- console.log(` [DRY] remove ${target}`);
- return;
- }
- fs.rmSync(target, { recursive: true, force: true });
- }
- function runNodeCheck(filePath) {
- const source = fs.readFileSync(filePath, 'utf8').replace(/^#!.*\r?\n/, '');
- try {
- // Syntax-only check without spawning a nested Node process. This keeps the
- // suite validator usable inside restricted runners that block child Node.
- new Function('require', 'module', 'exports', '__filename', '__dirname', source);
- } catch (error) {
- throw new Error(`syntax check failed for ${filePath}: ${error.message}`);
- }
- }
- function validateSuite() {
- console.log('[validate] douyin-speaking-daily suite');
- const missing = [];
- for (const rel of REQUIRED_FILES) {
- const file = path.join(PROJECT_ROOT, rel);
- if (!fs.existsSync(file)) missing.push(rel);
- }
- for (const skill of SKILLS) {
- const dir = findSkillDir(skill);
- if (!dir) {
- missing.push(`skill:${skill}`);
- continue;
- }
- const apiConfig = path.join(dir, 'api-config.json');
- if (fs.existsSync(apiConfig)) readJson(apiConfig);
- }
- for (const file of [
- 'scripts/tools/douyin-speaking-profile-memory.js',
- 'scripts/tools/douyin-speaking-daily-runner.js',
- 'scripts/tools/douyin-speaking-daily-report.js',
- 'scripts/tools/douyin-speaking-daily-p1-smoke.js',
- 'scripts/tools/douyin-viral-script-analyzer.js',
- 'scripts/tools/douyin-video-transcriber.js'
- ]) {
- runNodeCheck(path.join(PROJECT_ROOT, file));
- }
- readJson(SUITE_MANIFEST);
- if (missing.length) {
- throw new Error(`Missing suite files:\n${missing.map(item => ` - ${item}`).join('\n')}`);
- }
- console.log(` ok: ${SKILLS.length} skills, ${TOOL_FILES.length} tools, workspace bundle present`);
- }
- function buildSuite(opts) {
- console.log('[build] dist/douyin-speaking-daily.zip');
- ensureDir(DIST_DIR);
- const args = [
- path.join('scripts', 'deploy', 'package-and-upload.js'),
- '--only', 'douyin-speaking-daily',
- '--skip-workshop',
- '--skip-all'
- ];
- if (!opts.upload) args.push('--skip-upload');
- const child = spawnSync(NODE_CMD, args, {
- cwd: PROJECT_ROOT,
- encoding: 'utf8',
- maxBuffer: 1024 * 1024 * 100
- });
- if (child.stdout) process.stdout.write(child.stdout);
- if (child.stderr) process.stderr.write(child.stderr);
- if (child.status !== 0) {
- const details = child.error ? ` (${child.error.code || child.error.name}: ${child.error.message})` : '';
- throw new Error(`package-and-upload failed with exit ${child.status}${details}`);
- }
- if (!fs.existsSync(PACKAGE_ZIP)) {
- throw new Error(`Expected package was not generated: ${PACKAGE_ZIP}`);
- }
- writeDistManifest({ upload: opts.upload });
- }
- function deploySuite(opts) {
- const openclawDir = path.resolve(opts.openclawDir);
- const skillsTarget = path.join(openclawDir, 'skills');
- const toolsTarget = path.join(openclawDir, 'tools');
- const workspaceTarget = path.join(openclawDir, 'workspace');
- const workspaceToolsTarget = path.join(workspaceTarget, 'scripts', 'tools');
- console.log(`[deploy] ${openclawDir}${opts.dryRun ? ' (dry run)' : ''}`);
- if (!opts.dryRun && !fs.existsSync(openclawDir)) {
- throw new Error(`OpenClaw directory not found: ${openclawDir}`);
- }
- for (const skill of SKILLS) {
- const srcDir = findSkillDir(skill);
- if (!srcDir) throw new Error(`Cannot deploy missing skill: ${skill}`);
- const destDir = path.join(skillsTarget, skill);
- removeDirInside(destDir, skillsTarget, opts);
- copyDirRecursive(srcDir, destDir, opts);
- console.log(` [skill] ${skill}`);
- }
- for (const tool of TOOL_FILES) {
- const src = path.join(PROJECT_ROOT, 'scripts', 'tools', tool);
- if (!fs.existsSync(src)) throw new Error(`Missing tool: ${src}`);
- for (const targetRoot of [toolsTarget, workspaceToolsTarget]) {
- const dest = path.join(targetRoot, tool);
- if (opts.dryRun) {
- console.log(` [DRY] ${src} -> ${dest}`);
- } else {
- ensureDir(path.dirname(dest));
- fs.copyFileSync(src, dest);
- }
- }
- console.log(` [tool] ${tool}`);
- }
- const bundleDest = path.join(workspaceTarget, 'douyin-speaking-daily');
- removeDirInside(bundleDest, workspaceTarget, opts);
- copyDirRecursive(SUITE_DIR, bundleDest, opts);
- console.log(' [workspace] douyin-speaking-daily');
- }
- function writeDistManifest(extra = {}) {
- const packageMeta = readJson(SUITE_MANIFEST);
- const zipStat = fs.existsSync(PACKAGE_ZIP) ? fs.statSync(PACKAGE_ZIP) : undefined;
- const manifest = {
- name: 'douyin-speaking-daily',
- version: packageMeta.version,
- generatedAt: new Date().toISOString(),
- packageZip: fs.existsSync(PACKAGE_ZIP) ? path.relative(PROJECT_ROOT, PACKAGE_ZIP).replace(/\\/g, '/') : '',
- packageZipBytes: zipStat ? zipStat.size : 0,
- skills: SKILLS,
- tools: TOOL_FILES,
- workspaceBundle: 'douyin-speaking-daily',
- installCommand: 'node install.js',
- localDeployCommand: 'node scripts/deploy/douyin-speaking-daily-suite.js --deploy',
- uploadCommand: 'node scripts/deploy/douyin-speaking-daily-suite.js --build --upload',
- ...extra
- };
- fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
- console.log(`[manifest] ${path.relative(PROJECT_ROOT, DIST_MANIFEST)}`);
- }
- function main() {
- const opts = parseArgs(process.argv.slice(2));
- if (opts.help) {
- console.log(usage());
- return;
- }
- try {
- if (opts.validate) validateSuite();
- if (opts.build) buildSuite(opts);
- if (opts.deploy) deploySuite(opts);
- if (!opts.build) writeDistManifest({ upload: opts.upload });
- console.log('[done] douyin-speaking-daily suite is ready');
- } catch (error) {
- console.error(`[error] ${error.message}`);
- process.exit(1);
- }
- }
- main();
|