#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const os = require('os'); const { spawnSync } = require('child_process'); const qiniu = require('qiniu'); const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); const SUITE_DIR = path.join(PROJECT_ROOT, 'openclaw-skills', 'industry-trend-intelligence'); const MANIFEST_PATH = path.join(SUITE_DIR, 'skill-package-manifest.json'); const DIST_DIR = path.join(PROJECT_ROOT, 'dist'); const PACKAGE_ID = 'industry-trend-intelligence'; const PACKAGE_ZIP = path.join(DIST_DIR, `${PACKAGE_ID}.zip`); const DIST_MANIFEST = path.join(DIST_DIR, `${PACKAGE_ID}-suite-manifest.json`); const TEMP_ROOT = path.join(os.tmpdir(), 'openclaw-pkg', PACKAGE_ID); const CDN_PREFIX = 'x/openclaw-skills/packages'; const CDN_KEY = `${CDN_PREFIX}/${PACKAGE_ID}.zip`; const CDN_DOMAIN = 'https://repos.fmode.cn'; const BUCKET = 'nova-repos'; const QINIU_ACCESS_KEY = process.env.QINIU_AK || ''; const QINIU_SECRET_KEY = process.env.QINIU_SK || ''; const REQUIRED_FILES = [ 'README.md', 'deployment.md', 'openclaw-startup.md', 'skill-package-manifest.json', 'install.js', 'memory-templates/industry-trend-profile.json', 'scripts/industry-trend-runner.js', 'scripts/industry-trend-report.js', 'scripts/industry-trend-profile-builder.js', 'scripts/industry-trend-memory.js', 'scripts/xiaohongshu-trend-collector.js', 'skills/industry-trend-runner/SKILL.md', 'skills/industry-trend-runner/api-config.json', 'skills/xiaohongshu-trend-collector/SKILL.md', 'skills/xiaohongshu-trend-collector/api-config.json' ]; function parseArgs(argv) { const opts = { validate: false, build: false, upload: false, testPackage: false, deploy: false, dryRun: false, openclawDir: path.join(os.homedir(), '.openclaw'), help: false }; for (const token of argv) { if (token === '--validate') opts.validate = true; else if (token === '--build') opts.build = true; else if (token === '--upload') opts.upload = true; else if (token === '--test-package') opts.testPackage = true; else if (token === '--deploy') opts.deploy = true; else if (token === '--dry-run') opts.dryRun = true; else if (token === '--all') { opts.validate = true; opts.build = true; opts.testPackage = true; opts.deploy = true; } else if (token === '--help' || token === '-h') { opts.help = true; } } if (!opts.validate && !opts.build && !opts.upload && !opts.testPackage && !opts.deploy && !opts.help) { opts.validate = true; opts.build = true; opts.testPackage = true; } return opts; } function usage() { return [ 'Usage:', ' node scripts/deploy/industry-trend-intelligence-suite.js', ' node scripts/deploy/industry-trend-intelligence-suite.js --validate', ' node scripts/deploy/industry-trend-intelligence-suite.js --build --test-package', ' node scripts/deploy/industry-trend-intelligence-suite.js --build --upload', ' node scripts/deploy/industry-trend-intelligence-suite.js --all', '', 'Default mode is --validate --build --test-package without upload.' ].join('\n'); } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function copyDirRecursive(srcDir, destDir) { ensureDir(destDir); let count = 0; 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); else if (entry.isFile()) { ensureDir(path.dirname(dest)); fs.copyFileSync(src, dest); count++; } } return count; } function runNode(args, cwd = PROJECT_ROOT) { const child = spawnSync(process.execPath, args, { cwd, 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) { throw new Error(`node ${args.join(' ')} failed with exit ${child.status}`); } } function validateSuite() { console.log('[validate] industry-trend-intelligence suite'); const missing = []; for (const rel of REQUIRED_FILES) { if (!fs.existsSync(path.join(SUITE_DIR, rel))) missing.push(rel); } const manifest = readJson(MANIFEST_PATH); for (const skill of [...manifest.skills, ...(manifest.dependencySkills || [])]) { const skillDir = path.join(SUITE_DIR, 'skills', skill); if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) missing.push(`skill:${skill}/SKILL.md`); const config = path.join(skillDir, 'api-config.json'); if (fs.existsSync(config)) readJson(config); } if (missing.length) { throw new Error(`Missing suite files:\n${missing.map(item => ` - ${item}`).join('\n')}`); } runNode(['openclaw-skills/industry-trend-intelligence/scripts/validate.js']); console.log(` ok: ${manifest.skills.length} suite skills + ${(manifest.dependencySkills || []).length} dependency skills`); } function buildSuite() { console.log('[build] dist/industry-trend-intelligence.zip'); ensureDir(DIST_DIR); if (fs.existsSync(TEMP_ROOT)) fs.rmSync(TEMP_ROOT, { recursive: true, force: true }); ensureDir(TEMP_ROOT); copyDirRecursive(SUITE_DIR, TEMP_ROOT); if (fs.existsSync(PACKAGE_ZIP)) fs.unlinkSync(PACKAGE_ZIP); const command = `Compress-Archive -Path '${TEMP_ROOT}\\*' -DestinationPath '${PACKAGE_ZIP}' -Force`; const child = spawnSync('powershell', ['-NoProfile', '-Command', command], { 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) throw new Error(`Compress-Archive failed with exit ${child.status}`); writeDistManifest({ uploaded: false }); const size = fs.statSync(PACKAGE_ZIP).size; console.log(` ok: ${PACKAGE_ZIP} (${(size / 1024).toFixed(0)} KB)`); } function testPackage() { console.log('[test-package] unzip + dry-run + sample smoke'); if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`); const extractDir = path.join(os.tmpdir(), 'openclaw-pkg-test', PACKAGE_ID); if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true }); ensureDir(extractDir); const command = `Expand-Archive -Path '${PACKAGE_ZIP}' -DestinationPath '${extractDir}' -Force`; const child = spawnSync('powershell', ['-NoProfile', '-Command', command], { 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) throw new Error(`Expand-Archive failed with exit ${child.status}`); runNode(['install.js', '--dry-run'], extractDir); runNode([ 'scripts/industry-trend-runner.js', '--profile', 'memory-templates/industry-trend-profile.json', '--collection-mode', 'sample', '--output', path.join(extractDir, 'outputs', 'sample-smoke'), '--result-prefix', 'PKG_SMOKE' ], extractDir); } function deploySuite(opts) { const installScript = path.join(SUITE_DIR, 'install.js'); const args = [installScript]; if (opts.dryRun) args.push('--dry-run'); runNode(args, PROJECT_ROOT); } function uploadFile(localFile, cdnKey) { return new Promise((resolve, reject) => { const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY); const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 }); const token = putPolicy.uploadToken(mac); const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 }); const formUploader = new qiniu.form_up.FormUploader(config); const putExtra = new qiniu.form_up.PutExtra(); formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => { if (err) return reject(err); if (info.statusCode === 200) return resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` }); reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`)); }); }); } async function uploadSuite() { console.log('[upload] qiniu nova-repos'); if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`); const result = await uploadFile(PACKAGE_ZIP, CDN_KEY); writeDistManifest({ uploaded: true, downloadUrl: result.url }); const urlsPath = path.join(DIST_DIR, 'download-urls.json'); const urls = fs.existsSync(urlsPath) ? readJson(urlsPath) : {}; urls[PACKAGE_ID] = result.url; fs.writeFileSync(urlsPath, `${JSON.stringify(urls, null, 2)}\n`, 'utf8'); console.log(` ok: ${result.url}`); } function writeDistManifest(extra = {}) { const manifest = readJson(MANIFEST_PATH); const stat = fs.existsSync(PACKAGE_ZIP) ? fs.statSync(PACKAGE_ZIP) : undefined; const output = { name: PACKAGE_ID, version: manifest.version, generatedAt: new Date().toISOString(), packageZip: fs.existsSync(PACKAGE_ZIP) ? path.relative(PROJECT_ROOT, PACKAGE_ZIP).replace(/\\/g, '/') : '', packageZipBytes: stat ? stat.size : 0, skills: manifest.skills, dependencySkills: manifest.dependencySkills || [], workspaceBundle: PACKAGE_ID, installCommand: 'node install.js', uploadCommand: 'node scripts/deploy/industry-trend-intelligence-suite.js --build --upload', ...extra }; fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(output, null, 2)}\n`, 'utf8'); console.log(`[manifest] ${path.relative(PROJECT_ROOT, DIST_MANIFEST)}`); } async function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.help) { console.log(usage()); return; } if (opts.validate) validateSuite(); if (opts.build) buildSuite(); if (opts.testPackage) testPackage(); if (opts.deploy) deploySuite(opts); if (opts.upload) await uploadSuite(); if (!opts.build) writeDistManifest({ uploaded: false }); console.log('[done] industry-trend-intelligence suite is ready'); } main().catch(error => { console.error(`[error] ${error.message}`); process.exit(1); });