industry-trend-intelligence-suite.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 qiniu = require('qiniu');
  7. const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
  8. const SUITE_DIR = path.join(PROJECT_ROOT, 'openclaw-skills', 'industry-trend-intelligence');
  9. const MANIFEST_PATH = path.join(SUITE_DIR, 'skill-package-manifest.json');
  10. const DIST_DIR = path.join(PROJECT_ROOT, 'dist');
  11. const PACKAGE_ID = 'industry-trend-intelligence';
  12. const PACKAGE_ZIP = path.join(DIST_DIR, `${PACKAGE_ID}.zip`);
  13. const DIST_MANIFEST = path.join(DIST_DIR, `${PACKAGE_ID}-suite-manifest.json`);
  14. const TEMP_ROOT = path.join(os.tmpdir(), 'openclaw-pkg', PACKAGE_ID);
  15. const CDN_PREFIX = 'x/openclaw-skills/packages';
  16. const CDN_KEY = `${CDN_PREFIX}/${PACKAGE_ID}.zip`;
  17. const CDN_DOMAIN = 'https://repos.fmode.cn';
  18. const BUCKET = 'nova-repos';
  19. const QINIU_ACCESS_KEY = process.env.QINIU_AK || '';
  20. const QINIU_SECRET_KEY = process.env.QINIU_SK || '';
  21. const REQUIRED_FILES = [
  22. 'README.md',
  23. 'deployment.md',
  24. 'openclaw-startup.md',
  25. 'skill-package-manifest.json',
  26. 'install.js',
  27. 'memory-templates/industry-trend-profile.json',
  28. 'scripts/industry-trend-runner.js',
  29. 'scripts/industry-trend-report.js',
  30. 'scripts/industry-trend-profile-builder.js',
  31. 'scripts/industry-trend-memory.js',
  32. 'scripts/xiaohongshu-trend-collector.js',
  33. 'skills/industry-trend-runner/SKILL.md',
  34. 'skills/industry-trend-runner/api-config.json',
  35. 'skills/xiaohongshu-trend-collector/SKILL.md',
  36. 'skills/xiaohongshu-trend-collector/api-config.json'
  37. ];
  38. function parseArgs(argv) {
  39. const opts = {
  40. validate: false,
  41. build: false,
  42. upload: false,
  43. testPackage: false,
  44. deploy: false,
  45. dryRun: false,
  46. openclawDir: path.join(os.homedir(), '.openclaw'),
  47. help: false
  48. };
  49. for (const token of argv) {
  50. if (token === '--validate') opts.validate = true;
  51. else if (token === '--build') opts.build = true;
  52. else if (token === '--upload') opts.upload = true;
  53. else if (token === '--test-package') opts.testPackage = true;
  54. else if (token === '--deploy') opts.deploy = true;
  55. else if (token === '--dry-run') opts.dryRun = true;
  56. else if (token === '--all') {
  57. opts.validate = true;
  58. opts.build = true;
  59. opts.testPackage = true;
  60. opts.deploy = true;
  61. } else if (token === '--help' || token === '-h') {
  62. opts.help = true;
  63. }
  64. }
  65. if (!opts.validate && !opts.build && !opts.upload && !opts.testPackage && !opts.deploy && !opts.help) {
  66. opts.validate = true;
  67. opts.build = true;
  68. opts.testPackage = true;
  69. }
  70. return opts;
  71. }
  72. function usage() {
  73. return [
  74. 'Usage:',
  75. ' node scripts/deploy/industry-trend-intelligence-suite.js',
  76. ' node scripts/deploy/industry-trend-intelligence-suite.js --validate',
  77. ' node scripts/deploy/industry-trend-intelligence-suite.js --build --test-package',
  78. ' node scripts/deploy/industry-trend-intelligence-suite.js --build --upload',
  79. ' node scripts/deploy/industry-trend-intelligence-suite.js --all',
  80. '',
  81. 'Default mode is --validate --build --test-package without upload.'
  82. ].join('\n');
  83. }
  84. function ensureDir(dir) {
  85. fs.mkdirSync(dir, { recursive: true });
  86. }
  87. function readJson(filePath) {
  88. return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  89. }
  90. function copyDirRecursive(srcDir, destDir) {
  91. ensureDir(destDir);
  92. let count = 0;
  93. for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
  94. const src = path.join(srcDir, entry.name);
  95. const dest = path.join(destDir, entry.name);
  96. if (entry.isDirectory()) count += copyDirRecursive(src, dest);
  97. else if (entry.isFile()) {
  98. ensureDir(path.dirname(dest));
  99. fs.copyFileSync(src, dest);
  100. count++;
  101. }
  102. }
  103. return count;
  104. }
  105. function runNode(args, cwd = PROJECT_ROOT) {
  106. const child = spawnSync(process.execPath, args, {
  107. cwd,
  108. encoding: 'utf8',
  109. maxBuffer: 1024 * 1024 * 100
  110. });
  111. if (child.stdout) process.stdout.write(child.stdout);
  112. if (child.stderr) process.stderr.write(child.stderr);
  113. if (child.status !== 0) {
  114. throw new Error(`node ${args.join(' ')} failed with exit ${child.status}`);
  115. }
  116. }
  117. function validateSuite() {
  118. console.log('[validate] industry-trend-intelligence suite');
  119. const missing = [];
  120. for (const rel of REQUIRED_FILES) {
  121. if (!fs.existsSync(path.join(SUITE_DIR, rel))) missing.push(rel);
  122. }
  123. const manifest = readJson(MANIFEST_PATH);
  124. for (const skill of [...manifest.skills, ...(manifest.dependencySkills || [])]) {
  125. const skillDir = path.join(SUITE_DIR, 'skills', skill);
  126. if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) missing.push(`skill:${skill}/SKILL.md`);
  127. const config = path.join(skillDir, 'api-config.json');
  128. if (fs.existsSync(config)) readJson(config);
  129. }
  130. if (missing.length) {
  131. throw new Error(`Missing suite files:\n${missing.map(item => ` - ${item}`).join('\n')}`);
  132. }
  133. runNode(['openclaw-skills/industry-trend-intelligence/scripts/validate.js']);
  134. console.log(` ok: ${manifest.skills.length} suite skills + ${(manifest.dependencySkills || []).length} dependency skills`);
  135. }
  136. function buildSuite() {
  137. console.log('[build] dist/industry-trend-intelligence.zip');
  138. ensureDir(DIST_DIR);
  139. if (fs.existsSync(TEMP_ROOT)) fs.rmSync(TEMP_ROOT, { recursive: true, force: true });
  140. ensureDir(TEMP_ROOT);
  141. copyDirRecursive(SUITE_DIR, TEMP_ROOT);
  142. if (fs.existsSync(PACKAGE_ZIP)) fs.unlinkSync(PACKAGE_ZIP);
  143. const command = `Compress-Archive -Path '${TEMP_ROOT}\\*' -DestinationPath '${PACKAGE_ZIP}' -Force`;
  144. const child = spawnSync('powershell', ['-NoProfile', '-Command', command], {
  145. cwd: PROJECT_ROOT,
  146. encoding: 'utf8',
  147. maxBuffer: 1024 * 1024 * 100
  148. });
  149. if (child.stdout) process.stdout.write(child.stdout);
  150. if (child.stderr) process.stderr.write(child.stderr);
  151. if (child.status !== 0) throw new Error(`Compress-Archive failed with exit ${child.status}`);
  152. writeDistManifest({ uploaded: false });
  153. const size = fs.statSync(PACKAGE_ZIP).size;
  154. console.log(` ok: ${PACKAGE_ZIP} (${(size / 1024).toFixed(0)} KB)`);
  155. }
  156. function testPackage() {
  157. console.log('[test-package] unzip + dry-run + sample smoke');
  158. if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`);
  159. const extractDir = path.join(os.tmpdir(), 'openclaw-pkg-test', PACKAGE_ID);
  160. if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true });
  161. ensureDir(extractDir);
  162. const command = `Expand-Archive -Path '${PACKAGE_ZIP}' -DestinationPath '${extractDir}' -Force`;
  163. const child = spawnSync('powershell', ['-NoProfile', '-Command', command], {
  164. cwd: PROJECT_ROOT,
  165. encoding: 'utf8',
  166. maxBuffer: 1024 * 1024 * 100
  167. });
  168. if (child.stdout) process.stdout.write(child.stdout);
  169. if (child.stderr) process.stderr.write(child.stderr);
  170. if (child.status !== 0) throw new Error(`Expand-Archive failed with exit ${child.status}`);
  171. runNode(['install.js', '--dry-run'], extractDir);
  172. runNode([
  173. 'scripts/industry-trend-runner.js',
  174. '--profile',
  175. 'memory-templates/industry-trend-profile.json',
  176. '--collection-mode',
  177. 'sample',
  178. '--output',
  179. path.join(extractDir, 'outputs', 'sample-smoke'),
  180. '--result-prefix',
  181. 'PKG_SMOKE'
  182. ], extractDir);
  183. }
  184. function deploySuite(opts) {
  185. const installScript = path.join(SUITE_DIR, 'install.js');
  186. const args = [installScript];
  187. if (opts.dryRun) args.push('--dry-run');
  188. runNode(args, PROJECT_ROOT);
  189. }
  190. function uploadFile(localFile, cdnKey) {
  191. return new Promise((resolve, reject) => {
  192. const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY);
  193. const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 });
  194. const token = putPolicy.uploadToken(mac);
  195. const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 });
  196. const formUploader = new qiniu.form_up.FormUploader(config);
  197. const putExtra = new qiniu.form_up.PutExtra();
  198. formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => {
  199. if (err) return reject(err);
  200. if (info.statusCode === 200) return resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` });
  201. reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`));
  202. });
  203. });
  204. }
  205. async function uploadSuite() {
  206. console.log('[upload] qiniu nova-repos');
  207. if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`);
  208. const result = await uploadFile(PACKAGE_ZIP, CDN_KEY);
  209. writeDistManifest({ uploaded: true, downloadUrl: result.url });
  210. const urlsPath = path.join(DIST_DIR, 'download-urls.json');
  211. const urls = fs.existsSync(urlsPath) ? readJson(urlsPath) : {};
  212. urls[PACKAGE_ID] = result.url;
  213. fs.writeFileSync(urlsPath, `${JSON.stringify(urls, null, 2)}\n`, 'utf8');
  214. console.log(` ok: ${result.url}`);
  215. }
  216. function writeDistManifest(extra = {}) {
  217. const manifest = readJson(MANIFEST_PATH);
  218. const stat = fs.existsSync(PACKAGE_ZIP) ? fs.statSync(PACKAGE_ZIP) : undefined;
  219. const output = {
  220. name: PACKAGE_ID,
  221. version: manifest.version,
  222. generatedAt: new Date().toISOString(),
  223. packageZip: fs.existsSync(PACKAGE_ZIP) ? path.relative(PROJECT_ROOT, PACKAGE_ZIP).replace(/\\/g, '/') : '',
  224. packageZipBytes: stat ? stat.size : 0,
  225. skills: manifest.skills,
  226. dependencySkills: manifest.dependencySkills || [],
  227. workspaceBundle: PACKAGE_ID,
  228. installCommand: 'node install.js',
  229. uploadCommand: 'node scripts/deploy/industry-trend-intelligence-suite.js --build --upload',
  230. ...extra
  231. };
  232. fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
  233. console.log(`[manifest] ${path.relative(PROJECT_ROOT, DIST_MANIFEST)}`);
  234. }
  235. async function main() {
  236. const opts = parseArgs(process.argv.slice(2));
  237. if (opts.help) {
  238. console.log(usage());
  239. return;
  240. }
  241. if (opts.validate) validateSuite();
  242. if (opts.build) buildSuite();
  243. if (opts.testPackage) testPackage();
  244. if (opts.deploy) deploySuite(opts);
  245. if (opts.upload) await uploadSuite();
  246. if (!opts.build) writeDistManifest({ uploaded: false });
  247. console.log('[done] industry-trend-intelligence suite is ready');
  248. }
  249. main().catch(error => {
  250. console.error(`[error] ${error.message}`);
  251. process.exit(1);
  252. });