collect_seed.js 4.86 KB
const fs = require('fs');
const path = require('path');
const https = require('https');

const crawlTime = new Date().toISOString();
const seeds = [];

function fetchJson(url) {
  return new Promise((resolve, reject) => {
    https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
      });
    }).on('error', reject);
  });
}

function fetchHttp(url) {
  return new Promise((resolve, reject) => {
    https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(data));
    }).on('error', reject);
  });
}

async function collectHn(query, platformLabel) {
  const url = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(query)}&tags=story&hitsPerPage=30`;
  const data = await fetchJson(url);
  for (const hit of data.hits || []) {
    seeds.push({
      id: `hn-${hit.objectID}`,
      platform: platformLabel,
      trend_type: 'discussion',
      title: hit.title,
      url: hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`,
      metric: hit.points || 0,
      metric_label: 'points',
      comments: hit.num_comments || 0,
      author: hit.author,
      created_at: new Date(hit.created_at_i * 1000).toISOString(),
      description: '',
      source_url: url,
      crawl_time: crawlTime
    });
  }
}

async function collectGitHub(query, platformLabel) {
  const url = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=updated&order=desc&per_page=30`;
  const data = await fetchJson(url);
  for (const item of data.items || []) {
    seeds.push({
      id: `gh-${item.id}`,
      platform: platformLabel,
      trend_type: 'repo',
      title: item.full_name,
      url: item.html_url,
      metric: item.stargazers_count || 0,
      metric_label: 'stars',
      language: item.language || '',
      updated_at: item.updated_at,
      description: (item.description || '').slice(0, 200),
      source_url: url,
      crawl_time: crawlTime
    });
  }
}

async function collectTikTokDiscover() {
  const url = 'https://www.tiktok.com/node/share/discover?noUser=1&userCount=50';
  const data = await fetchJson(url);
  const list = data.body?.[0]?.exploreList || [];
  for (const x of list) {
    const ci = x.cardItem;
    seeds.push({
      id: `tt-${ci.id}`,
      platform: 'tiktok',
      trend_type: 'hashtag',
      title: ci.title,
      url: `https://www.tiktok.com${ci.link}`,
      metric: ci.extraInfo?.views || 0,
      metric_label: 'views',
      cover: ci.cover || '',
      description: (ci.description || '').slice(0, 200),
      source_url: url,
      crawl_time: crawlTime
    });
  }
}

async function collectProductHunt() {
  const url = 'https://www.producthunt.com/feed?category=tech';
  const xml = await fetchHttp(url);
  const entryRegex = /<entry>([\s\S]*?)<\/entry>/g;
  let m;
  while ((m = entryRegex.exec(xml)) !== null) {
    const entry = m[1];
    const idMatch = entry.match(/<id>([^<]+)<\/id>/);
    const titleMatch = entry.match(/<title>([^<]+)<\/title>/);
    const linkMatch = entry.match(/<link rel="alternate"[^>]+href="([^"]+)"/);
    const contentMatch = entry.match(/<content type="html"><!\[CDATA\[([\s\S]*?)\]\]>/);
    const publishedMatch = entry.match(/<published>([^<]+)<\/published>/);
    seeds.push({
      id: `ph-${(idMatch?.[1] || '').replace(/.*\//, '')}`,
      platform: 'product_hunt',
      trend_type: 'product',
      title: titleMatch?.[1] || '',
      url: linkMatch?.[1] || '',
      metric: 0,
      metric_label: 'votes',
      description: ((contentMatch?.[1] || '').replace(/<[^>]+>/g, ' ').trim()).slice(0, 200),
      published_at: publishedMatch?.[1] || '',
      source_url: url,
      crawl_time: crawlTime
    });
  }
}

(async () => {
  await collectTikTokDiscover();
  await collectHn('tiktok', 'hn_tiktok');
  await collectHn('youtube shorts', 'hn_youtube_shorts');
  await collectHn('instagram reels', 'hn_instagram_reels');
  await collectHn('twitter video', 'hn_twitter_video');
  await collectGitHub('tiktok', 'github_tiktok');
  await collectGitHub('youtube shorts', 'github_youtube_shorts');
  await collectGitHub('instagram reels', 'github_instagram_reels');
  await collectProductHunt();

  // 落点必须由本文件位置推导,不能写相对 cwd 的路径:本脚本由 agent 运行时调用,
  // cwd 是任务态目录(system/agents/{aid}/{cid}/{taskId}/),裸相对路径会在 cwd 下
  // 造出一棵野生嵌套树而不是写到目标本体里(28 号改造前真的发生过)。
  const outPath = path.resolve(__dirname, '../trend-scout-global/train/hunt/seed_data.json');
  fs.writeFileSync(outPath, JSON.stringify(seeds, null, 2));
  console.log(`Wrote ${seeds.length} seeds to ${outPath}`);
})();