collect_centrum_douyin_live.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { collectLiveDataset } = require('../claude-code/claude-code-voc-intelligence/mcp/src/features/douyin-trend/live-collector');
  5. function parseArgs(argv) {
  6. const args = {};
  7. for (let i = 0; i < argv.length; i += 1) {
  8. const token = argv[i];
  9. if (!token.startsWith('--')) continue;
  10. const eq = token.indexOf('=');
  11. if (eq > 0) {
  12. args[token.slice(2, eq)] = token.slice(eq + 1);
  13. continue;
  14. }
  15. const key = token.slice(2);
  16. const next = argv[i + 1];
  17. if (next && !next.startsWith('--')) {
  18. args[key] = next;
  19. i += 1;
  20. } else {
  21. args[key] = true;
  22. }
  23. }
  24. return args;
  25. }
  26. function splitList(value) {
  27. if (!value) return [];
  28. if (Array.isArray(value)) return value;
  29. return String(value)
  30. .split(/[,,、\n\r]+/)
  31. .map(item => item.trim())
  32. .filter(Boolean);
  33. }
  34. function numberArg(value, fallback) {
  35. const number = Number(value);
  36. return Number.isFinite(number) && number >= 0 ? Math.round(number) : fallback;
  37. }
  38. function videoKey(video) {
  39. return String(video.id || video.videoId || video.url || '').trim();
  40. }
  41. function commentKey(comment) {
  42. return String(comment.id || comment.cid || `${comment.videoId}:${comment.text}`).trim();
  43. }
  44. function dedupeBy(items, keyFn) {
  45. const map = new Map();
  46. for (const item of items) {
  47. const key = keyFn(item);
  48. if (!key) continue;
  49. if (!map.has(key)) map.set(key, item);
  50. }
  51. return [...map.values()];
  52. }
  53. async function main() {
  54. const args = parseArgs(process.argv.slice(2));
  55. const token = process.env.VOC_TOKEN || process.env.VOC_DOUYIN_TOKEN || process.env.DOUYIN_TOKEN || '';
  56. if (!token) {
  57. throw new Error('VOC_TOKEN is required in process env.');
  58. }
  59. const output = path.resolve(args.output || 'outputs/centrum_douyin_audit_2026-06-08/live-full/live-raw-dataset.json');
  60. const keywords = splitList(args.keywords);
  61. if (!keywords.length) throw new Error('--keywords is required.');
  62. const profile = {
  63. project: args.project || 'Centrum Douyin VOC audit',
  64. industry: args.industry || 'nutrition supplements',
  65. targetAudience: splitList(args.audience || 'adults,30+,silver age,parents'),
  66. keywords,
  67. mustTrackSignals: ['price', 'authenticity', 'version', 'effect', 'energy', 'brain', 'pregnancy', 'dosage', 'ingredients']
  68. };
  69. const startedAt = new Date().toISOString();
  70. const result = await collectLiveDataset({
  71. profile,
  72. token,
  73. options: {
  74. keywordLimit: numberArg(args.keywordLimit || args['keyword-limit'], keywords.length),
  75. videosPerKeyword: numberArg(args.videosPerKeyword || args['videos-per-keyword'], 10),
  76. maxCommentPages: numberArg(args.maxCommentPages || args['max-comment-pages'], 3),
  77. sortType: args.sortType || args.sort || args['sort-type'] || '1',
  78. publishTime: args.publishTime || args['publish-time'] || '180',
  79. filterDuration: args.filterDuration || args['filter-duration'] || '0',
  80. contentType: args.contentType || args['content-type'] || '1',
  81. baseUrl: args.baseUrl || args['base-url'] || process.env.DOUYIN_API_BASE_URL || process.env.VOC_DOUYIN_API_BASE_URL
  82. }
  83. });
  84. const videos = dedupeBy(result.videos || [], videoKey);
  85. const comments = dedupeBy(result.comments || [], commentKey);
  86. const doc = {
  87. generatedAt: new Date().toISOString(),
  88. startedAt,
  89. profile,
  90. requested: {
  91. keywordCount: keywords.length,
  92. keywordLimit: numberArg(args.keywordLimit || args['keyword-limit'], keywords.length),
  93. videosPerKeyword: numberArg(args.videosPerKeyword || args['videos-per-keyword'], 10),
  94. maxCommentPages: numberArg(args.maxCommentPages || args['max-comment-pages'], 3)
  95. },
  96. effectiveKeywords: result.keywords || [],
  97. summary: {
  98. videoCount: videos.length,
  99. commentCount: comments.length,
  100. warningCount: (result.warnings || []).length,
  101. errorCount: (result.errors || []).length
  102. },
  103. videos,
  104. comments,
  105. warnings: result.warnings || [],
  106. errors: result.errors || []
  107. };
  108. fs.mkdirSync(path.dirname(output), { recursive: true });
  109. fs.writeFileSync(output, JSON.stringify(doc, null, 2), 'utf8');
  110. console.log(JSON.stringify({
  111. status: 'ok',
  112. output,
  113. summary: doc.summary,
  114. effectiveKeywordCount: doc.effectiveKeywords.length
  115. }, null, 2));
  116. }
  117. main().catch(error => {
  118. console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2));
  119. process.exit(1);
  120. });