Skill: peterdog666/hotspot-radar (version 1.0.0)

--- name: hotspot-radar description: 全网热榜追踪器，聚合微博/知乎/抖音/B站/小红书热搜榜单，支持趋势分析、话题监控和定时推送 (user) allowed-tools: WebFetch, WebSearch, Read, Write, ExecuteCommand disable-model-invocation: false user-invocable: true --- # 热点雷达 - 全网热榜追踪器 你是一个全网热点信息追踪专家，能够实时聚合微博、知乎、抖音、B站、小红书五大平台的热搜榜单，为用户提供一站式的热点发现与分析服务。 ## 核心能力 ### 1. 实时热榜获取 - 微博热搜榜 - 知乎热榜 - 抖音热搜榜 - B站热榜 - 小红书热榜 ### 2. 趋势追踪分析 - 对比昨日热榜，发现新上榜/落榜话题 - 计算话题热度变化趋势 - 识别"正在起飞"的热点 ### 3. 话题监控 - 用户设定关键词监控（如"AI"、"考研"、"新能源"） - 当监控话题出现在热榜时及时提醒 - 支持多关键词同时监控 ### 4. 定时报告推送 - 每日早报（9:00）和晚报（21:00） - 自动生成Markdown格式热点报告 - 支持推送到飞书群 ### 5. HTML可视化报告 - 生成交互式HTML报告（深色主题） - 内嵌Chart.js图表：各平台热度对比柱状图、新增/上升话题横向条形图 - 平台热榜卡片式展示，支持TOP10详情 - 跨平台热点聚合展示 - 话题监控告警高亮卡片 - 命令：`node scripts/index.js html` ## 标准工作流程 ### 获取全网热榜 1. 并行调用各平台数据采集接口 2. 统一数据格式，按热度排序 3. 生成平台对比表格 4. 输出Markdown格式热榜报告 ### 趋势分析模式 1. 读取历史热榜数据（存储在 `data/history/` 目录） 2. 与当前热榜对比 3. 标注：新上榜(↑NEW)、排名上升(↑)、排名下降(↓)、落榜(↓OUT) 4. 生成趋势变化摘要 ### 话题监控模式 1. 读取监控配置（`config/monitor.json`） 2. 在当前热榜中搜索匹配话题 3. 如有匹配，生成提醒报告 4. 更新监控日志 ### 生成报告 报告格式： ```markdown # 🌐 全网热点日报 | {日期} ## 今日概览 - 微博热搜：{条数}条 | 知乎热榜：{条数}条 | 抖音热搜：{条数}条 - B站热榜：{条数}条 | 小红书热榜：{条数}条 - 发现 {n} 个跨平台热点话题 ## 跨平台热点 TOP5 1. [话题名称] - 出现在微博、知乎、抖音 2. ... ## 各平台热榜 ### 微博热搜 | 排名 | 话题 | 热度 | |------|------|------| | 1 | xxx | 1234万 | ... (其他平台同理) ## 趋势变化 ### 🆕 新上榜 - [话题] - 首度登榜 ### 📈 热度上升 - [话题] - 从第X名升至第X名 ### 📉 落榜 - [话题] - 昨日第X名，今日跌出榜单 ## 话题监控提醒 - [关键词]: 出现在微博第X位、知乎第X位 --- 生成时间: {timestamp} ``` ## 数据存储 - `data/history/{platform}/{YYYY-MM-DD}.json` - 各平台历史热榜 - `data/trends/{YYYY-MM-DD}.json` - 每日趋势分析结果 - `config/monitor.json` - 话题监控配置 - `config/push.json` - 推送渠道配置 ## 错误处理 - 某平台获取失败时，跳过该平台并标注"数据获取失败" - 不影响其他平台正常展示 - 自动重试机制：首次失败后等待3秒重试，最多3次 ## 注意事项 - 尊重平台数据版权，仅展示话题名称，不深度爬取详细内容 - 定时任务需要用户确认推送渠道配置 - 首次使用需运行初始化脚本配置数据目录

path: package.json
content: {
  "name": "hotspot-radar",
  "version": "1.1.0",
  "description": "全网热榜追踪器 - 聚合微博/知乎/抖音/B站/小红书热搜榜单",
  "main": "scripts/index.js",
  "scripts": {
    "start": "node scripts/index.js",
    "fetch": "node scripts/index.js fetch",
    "report": "node scripts/index.js report",
    "html": "node scripts/index.js html",
    "quick": "node scripts/index.js quick",
    "monitor": "node scripts/monitor.js",
    "init": "node scripts/index.js init"
  },
  "keywords": [
    "hotspot",
    "trending",
    "social-media",
    "weibo",
    "zhihu",
    "bilibili",
    "douyin",
    "xiaohongshu"
  ],
  "author": "",
  "license": "MIT",
  "engines": {
    "node": ">=14.0.0"
  },
  "dependencies": {}
}


path: README.md
content: # 🌐 热点雷达 - 全网热榜追踪器

> 聚合微博/知乎/抖音/B站/小红书热搜榜单，支持趋势分析和话题监控

## 功能特性

- ✅ **实时采集** - 并行获取五大平台热榜数据
- ✅ **趋势追踪** - 对比昨日发现新上榜/落榜话题
- ✅ **跨平台热点** - 发现同时在多个平台发酵的话题
- ✅ **话题监控** - 关键词订阅，有动态时及时提醒
- ✅ **每日报告** - 自动生成Markdown格式热点日报
- ✅ **HTML可视化报告** - 交互式图表报告，含热度对比图/趋势图

## 快速开始

```bash
# 初始化目录
node scripts/index.js init

# 一键采集+生成报告
node scripts/index.js quick

# 单独采集数据
node scripts/index.js fetch

# 单独生成Markdown报告
node scripts/index.js report

# 生成HTML可视化报告（含图表）
node scripts/index.js html
```

## 目录结构

```
hotspot-radar/
├── SKILL.md              # Skill定义文件
├── README.md             # 使用文档
├── package.json          # NPM配置
├── data/                 # 数据存储
│   ├── history/         # 历史热榜
│   │   ├── weibo/
│   │   ├── zhihu/
│   │   ├── bilibili/
│   │   ├── douyin/
│   │   └── xiaohongshu/
│   ├── trends/          # 每日趋势快照
│   └── reports/         # 生成的报告
├── config/              # 配置文件
│   ├── monitor.json    # 监控关键词
│   └── push.json       # 推送配置
├── scripts/             # 脚本文件
│   ├── index.js        # 主入口
│   ├── collector.js    # 数据采集
│   ├── reporter.js     # Markdown报告生成
│   ├── htmlReporter.js  # HTML可视化报告生成
│   └── monitor.js      # 监控管理
├── references/          # 参考文档
└── assets/             # 资源文件
```

## 话题监控

```bash
# 添加监控关键词
node scripts/monitor.js add "人工智能"

# 移除监控关键词
node scripts/monitor.js remove "人工智能"

# 列出所有关键词
node scripts/monitor.js list

# 查看监控状态
node scripts/monitor.js status
```

## 定时任务配置

使用WorkBuddy的automation功能设置每日定时推送：

1. 打开WorkBuddy设置
2. 创建新的automation
3. 设置触发时间（如每天9:00）
4. 设置任务为 `node scripts/index.js quick`

## 环境变量（可选）

```bash
# 微博Cookie（提高API限制）
export WEIBO_COOKIE="your_weibo_cookie"

# 小红书Cookie（提高API限制）
export XHS_COOKIE="your_xhs_cookie"
```

## 数据来源

| 平台 | 数据接口 | 说明 |
|------|----------|------|
| 微博 | 热搜API | 实时更新 |
| 知乎 | 热榜API | 实时更新 |
| 抖音 | 热榜API | 需device_id |
| B站 | 排行榜API | 实时更新 |
| 小红书 | 搜索热词API | 实时更新 |

## 开源协议

MIT License

---

*由「热点雷达」Skill v1.0.0 提供*


path: scripts/collector.js
content: /**
 * 热点雷达 - 数据采集器 v4
 * 微博: 官方API
 * 知乎/抖音/B站: 洛樱云API
 * 小红书: 60s API (v2/rednote)
 */

const https = require('https');
const fs = require('fs');
const path = require('path');

// 配置
const CONFIG = {
  dataDir: path.join(__dirname, '../data'),
  historyDir: path.join(__dirname, '../data/history'),
  configDir: path.join(__dirname, '../config'),
  // API接口地址
  API: {
    // 微博官方API
    WEIBO: 'https://weibo.com/ajax/statuses/hot_band',
    // 洛樱云API - 知乎、抖音、B站
    LUOYING: 'https://apiserver.alcex.cn/daily-hot/',
  }
};

// 确保目录存在
function ensureDirs() {
  var dirs = [
    CONFIG.dataDir,
    CONFIG.historyDir,
    path.join(CONFIG.historyDir, 'weibo'),
    path.join(CONFIG.historyDir, 'zhihu'),
    path.join(CONFIG.historyDir, 'bilibili'),
    path.join(CONFIG.historyDir, 'douyin'),
    path.join(CONFIG.historyDir, 'xiaohongshu'),
    path.join(CONFIG.dataDir, 'trends'),
    CONFIG.configDir,
  ];
  dirs.forEach(function(dir) {
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
  });
}

// HTTP请求封装
function fetch(url, options) {
  return new Promise(function(resolve, reject) {
    var protocol = url.startsWith('https') ? https : require('http');
    var requestOptions = {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Accept': 'application/json, text/html',
        'Referer': 'https://weibo.com',
      },
      timeout: 15000,
    };

    protocol.get(url, requestOptions, function(res) {
      var data = '';
      res.on('data', function(chunk) { data += chunk; });
      res.on('end', function() {
        try {
          resolve(JSON.parse(data));
        } catch (e) {
          resolve({ raw: data });
        }
      });
    }).on('error', function(err) {
      reject(err);
    });
  });
}

// 获取微博热搜
async function fetchWeibo() {
  try {
    console.log('  [微博] 开始获取...');
    var data = await fetch(CONFIG.API.WEIBO);
    var items = data.data && data.data.band_list ? data.data.band_list : [];
    var result = items.map(function(item, idx) {
      return {
        rank: idx + 1,
        topic: item.word || item.topic_name,
        hotValue: item.raw_hot || item.num || 0,
        label: item.label_name || '',
        url: 'https://s.weibo.com/weibo?q=' + encodeURIComponent(item.word || item.topic_name)
      };
    });
    console.log('  [微博] 获取成功: ' + result.length + '条');
    return result;
  } catch (e) {
    console.log('  [微博] 获取失败: ' + e.message);
    return [];
  }
}

// 获取知乎热搜 - 使用洛樱云API
async function fetchZhihu() {
  try {
    console.log('  [知乎] 开始获取...');
    var data = await fetch(CONFIG.API.LUOYING + 'zhihu');
    if (data.code === 200 && data.data) {
      var result = data.data.map(function(item, idx) {
        return {
          rank: idx + 1,
          topic: item.title,
          hotValue: item.hot || 0,
          label: '',
          url: item.url || ''
        };
      });
      console.log('  [知乎] 获取成功: ' + result.length + '条');
      return result;
    }
    console.log('  [知乎] 接口返回异常');
    return [];
  } catch (e) {
    console.log('  [知乎] 获取失败: ' + e.message);
    return [];
  }
}

// 获取B站热搜 - 使用洛樱云API
async function fetchBilibili() {
  try {
    console.log('  [B站] 开始获取...');
    var data = await fetch(CONFIG.API.LUOYING + 'bilibili');
    if (data.code === 200 && data.data) {
      var result = data.data.map(function(item, idx) {
        return {
          rank: idx + 1,
          topic: item.title,
          hotValue: item.hot || 0,
          label: item.desc || '',
          url: item.url || ''
        };
      });
      console.log('  [B站] 获取成功: ' + result.length + '条');
      return result;
    }
    console.log('  [B站] 接口返回异常');
    return [];
  } catch (e) {
    console.log('  [B站] 获取失败: ' + e.message);
    return [];
  }
}

// 获取抖音热搜 - 使用洛樱云API
async function fetchDouyin() {
  try {
    console.log('  [抖音] 开始获取...');
    var data = await fetch(CONFIG.API.LUOYING + 'douyin');
    if (data.code === 200 && data.data) {
      var result = data.data.map(function(item, idx) {
        return {
          rank: idx + 1,
          topic: item.title,
          hotValue: item.hot || 0,
          label: '',
          url: item.url || ''
        };
      });
      console.log('  [抖音] 获取成功: ' + result.length + '条');
      return result;
    }
    console.log('  [抖音] 接口返回异常');
    return [];
  } catch (e) {
    console.log('  [抖音] 获取失败: ' + e.message);
    return [];
  }
}

// 获取小红书热搜 - 使用60s API (v2)
async function fetchXiaohongshu() {
  try {
    console.log('  [小红书] 开始获取...');
    var data = await fetch('https://60s.viki.moe/v2/rednote');
    if (data.code === 200 && data.data) {
      var result = data.data.map(function(item, idx) {
        return {
          rank: item.rank || idx + 1,
          topic: item.title,
          hotValue: item.score || 0,
          label: item.word_type || '',
          url: item.link || ''
        };
      });
      console.log('  [小红书] 获取成功: ' + result.length + '条');
      return result;
    }
    console.log('  [小红书] 接口返回异常');
    return [];
  } catch (e) {
    console.log('  [小红书] 获取失败: ' + e.message);
    return [];
  }
}

// 获取所有平台热榜
async function fetchAll() {
  console.log('\n开始采集全网热榜...\n');

  var results = await Promise.all([
    fetchWeibo(),
    fetchZhihu(),
    fetchBilibili(),
    fetchDouyin(),
    fetchXiaohongshu()
  ]);

  var data = {
    weibo: results[0] || [],
    zhihu: results[1] || [],
    bilibili: results[2] || [],
    douyin: results[3] || [],
    xiaohongshu: results[4] || [],
    timestamp: new Date().toISOString()
  };

  console.log('\n采集完成:', {
    weibo: data.weibo.length,
    zhihu: data.zhihu.length,
    bilibili: data.bilibili.length,
    douyin: data.douyin.length,
    xiaohongshu: data.xiaohongshu.length
  });

  return data;
}

// 保存历史数据
function saveHistory(data) {
  var today = new Date().toISOString().split('T')[0];
  var platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];

  platforms.forEach(function(platform) {
    var filePath = path.join(CONFIG.historyDir, platform, today + '.json');
    fs.writeFileSync(filePath, JSON.stringify(data[platform], null, 2), 'utf-8');
  });

  // 保存完整快照
  var snapshotPath = path.join(CONFIG.dataDir, 'trends', today + '.json');
  fs.writeFileSync(snapshotPath, JSON.stringify(data, null, 2), 'utf-8');

  console.log('历史数据已保存: ' + today);
}

// 主函数
async function main() {
  ensureDirs();
  var data = await fetchAll();
  saveHistory(data);

  console.log('\n========== 今日热榜汇总 ==========');
  console.log('- 微博: ' + data.weibo.length + '条');
  console.log('- 知乎: ' + data.zhihu.length + '条');
  console.log('- B站: ' + data.bilibili.length + '条');
  console.log('- 抖音: ' + data.douyin.length + '条');
  console.log('- 小红书: ' + data.xiaohongshu.length + '条');
  console.log('================================\n');

  return data;
}

// 导出
module.exports = {
  fetchAll: fetchAll,
  saveHistory: saveHistory,
  fetchWeibo: fetchWeibo,
  fetchZhihu: fetchZhihu,
  fetchBilibili: fetchBilibili,
  fetchDouyin: fetchDouyin,
  fetchXiaohongshu: fetchXiaohongshu,
  ensureDirs: ensureDirs
};

// 直接运行
if (require.main === module) {
  main().then(function(data) {
    console.log('数据采集完成');
    process.exit(0);
  }).catch(function(e) {
    console.error('采集失败:', e);
    process.exit(1);
  });
}


path: scripts/htmlReporter.js
content: /**
 * 热点雷达 - HTML可视化报告生成器
 * 生成带图表的交互式HTML报告
 */

const fs = require('fs');
const path = require('path');

const CONFIG = {
  dataDir: path.join(__dirname, '../data'),
  historyDir: path.join(__dirname, '../data/history'),
  configDir: path.join(__dirname, '../config'),
};

// 格式化日期
function formatDate(date = new Date()) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return year + '年' + month + '月' + day + '日';
}

// 格式化时间
function formatTime(date = new Date()) {
  const h = String(date.getHours()).padStart(2, '0');
  const m = String(date.getMinutes()).padStart(2, '0');
  const s = String(date.getSeconds()).padStart(2, '0');
  return h + ':' + m + ':' + s;
}

// 格式化热度值
function formatHotValue(value) {
  if (value >= 100000000) {
    return (value / 100000000).toFixed(1) + '亿';
  } else if (value >= 10000) {
    return (value / 10000).toFixed(1) + '万';
  }
  return value.toString();
}

// 读取历史数据
function loadHistory(platform, daysAgo = 1) {
  const date = new Date();
  date.setDate(date.getDate() - daysAgo);
  const fileName = date.toISOString().split('T')[0] + '.json';
  const filePath = path.join(CONFIG.historyDir, platform, fileName);

  try {
    if (fs.existsSync(filePath)) {
      return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
    }
  } catch (e) {
    console.error('Read history failed [' + platform + '] [' + daysAgo + 'days ago]:', e.message);
  }
  return [];
}

// 读取监控配置
function loadMonitorConfig() {
  const configPath = path.join(CONFIG.configDir, 'monitor.json');
  try {
    if (fs.existsSync(configPath)) {
      return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    }
  } catch (e) {
    console.error('Read monitor config failed:', e.message);
  }
  return { keywords: [] };
}

// 平台配置
const PLATFORMS = {
  weibo: { name: '微博', color: '#E6162D', bgColor: 'rgba(230,22,45,0.1)' },
  zhihu: { name: '知乎', color: '#0066FF', bgColor: 'rgba(0,102,255,0.1)' },
  bilibili: { name: 'B站', color: '#FB7299', bgColor: 'rgba(251,114,153,0.1)' },
  douyin: { name: '抖音', color: '#161823', bgColor: 'rgba(22,24,35,0.1)' },
  xiaohongshu: { name: '小红书', color: '#FF2442', bgColor: 'rgba(255,36,66,0.1)' },
};

// 趋势分析
function analyzeTrends(currentData) {
  const platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];
  const platformNames = { weibo: '微博', zhihu: '知乎', bilibili: 'B站', douyin: '抖音', xiaohongshu: '小红书' };

  const trends = {
    newTopics: [],
    risingTopics: [],
    fallingTopics: [],
    outTopics: [],
  };

  platforms.forEach(function (platform) {
    const current = currentData[platform] || [];
    const yesterday = loadHistory(platform, 1);

    if (yesterday.length === 0) return;

    const yesterdayTopics = new Map(yesterday.map(function (item) { return [item.topic, item.rank]; }));

    current.forEach(function (item) {
      const prevRank = yesterdayTopics.get(item.topic);
      if (prevRank === undefined) {
        trends.newTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          platformKey: platform,
          currentRank: item.rank,
          hotValue: item.hotValue,
        });
      } else if (item.rank < prevRank) {
        trends.risingTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          platformKey: platform,
          fromRank: prevRank,
          toRank: item.rank,
          delta: prevRank - item.rank,
          hotValue: item.hotValue,
        });
      } else if (item.rank > prevRank) {
        trends.fallingTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          platformKey: platform,
          fromRank: prevRank,
          toRank: item.rank,
          delta: item.rank - prevRank,
          hotValue: item.hotValue,
        });
      }
    });

    yesterday.forEach(function (item) {
      const stillExists = current.some(function (c) { return c.topic === item.topic; });
      if (!stillExists && item.rank <= 10) {
        trends.outTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          prevRank: item.rank,
        });
      }
    });
  });

  return trends;
}

// 跨平台热点分析
function findCrossPlatformTopics(currentData) {
  const platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];
  const platformNames = { weibo: '微博', zhihu: '知乎', bilibili: 'B站', douyin: '抖音', xiaohongshu: '小红书' };

  const topicMap = new Map();

  platforms.forEach(function (platform) {
    const data = currentData[platform] || [];
    data.slice(0, 20).forEach(function (item) {
      const normalizedTopic = item.topic.toLowerCase().trim();
      if (!topicMap.has(normalizedTopic)) {
        topicMap.set(normalizedTopic, { topic: item.topic, platforms: [], platformKeys: [] });
      }
      const entry = topicMap.get(normalizedTopic);
      if (!entry.platforms.includes(platformNames[platform])) {
        entry.platforms.push(platformNames[platform]);
        entry.platformKeys.push(platform);
      }
    });
  });

  const crossPlatform = [];
  topicMap.forEach(function (value) {
    if (value.platforms.length >= 2) {
      crossPlatform.push(value);
    }
  });

  crossPlatform.sort(function (a, b) { return b.platforms.length - a.platforms.length; });

  return crossPlatform.slice(0, 10);
}

// 话题监控检查
function checkMonitorKeywords(currentData) {
  const config = loadMonitorConfig();
  if (!config.keywords || config.keywords.length === 0) {
    return [];
  }

  const platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];
  const platformNames = { weibo: '微博', zhihu: '知乎', bilibili: 'B站', douyin: '抖音', xiaohongshu: '小红书' };
  const matches = [];

  platforms.forEach(function (platform) {
    const data = currentData[platform] || [];
    data.forEach(function (item) {
      const topicLower = item.topic.toLowerCase();
      config.keywords.forEach(function (keyword) {
        if (topicLower.includes(keyword.toLowerCase())) {
          matches.push({
            keyword: keyword,
            topic: item.topic,
            platform: platformNames[platform],
            platformKey: platform,
            rank: item.rank,
            hotValue: item.hotValue,
          });
        }
      });
    });
  });

  return matches;
}

// 生成HTML报告
function generateHtmlReport(currentData) {
  const today = new Date();
  const dateStr = formatDate(today);
  const timeStr = formatTime(today);
  const timestamp = today.toISOString();

  const counts = {
    weibo: currentData.weibo ? currentData.weibo.length : 0,
    zhihu: currentData.zhihu ? currentData.zhihu.length : 0,
    bilibili: currentData.bilibili ? currentData.bilibili.length : 0,
    douyin: currentData.douyin ? currentData.douyin.length : 0,
    xiaohongshu: currentData.xiaohongshu ? currentData.xiaohongshu.length : 0,
  };

  const trends = analyzeTrends(currentData);
  const crossPlatform = findCrossPlatformTopics(currentData);
  const monitorMatches = checkMonitorKeywords(currentData);

  // 构建平台数据JS
  const platformDataJson = JSON.stringify({
    weibo: (currentData.weibo || []).slice(0, 10),
    zhihu: (currentData.zhihu || []).slice(0, 10),
    bilibili: (currentData.bilibili || []).slice(0, 10),
    douyin: (currentData.douyin || []).slice(0, 10),
    xiaohongshu: (currentData.xiaohongshu || []).slice(0, 10),
  });

  // 跨平台数据
  const crossPlatformJson = JSON.stringify(crossPlatform);

  // 趋势数据
  const trendsJson = JSON.stringify({
    new: trends.newTopics.slice(0, 10),
    rising: trends.risingTopics.slice(0, 10),
    falling: trends.fallingTopics.slice(0, 10),
    out: trends.outTopics.slice(0, 10),
  });

  // 监控告警
  const monitorJson = JSON.stringify(monitorMatches);

  const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>热点雷达 | ` + dateStr + `</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  <style>
    :root {
      --bg: #0f0f1a;
      --card: #1a1a2e;
      --card-hover: #22223a;
      --border: #2a2a4a;
      --text: #e0e0f0;
      --text-dim: #8888aa;
      --weibo: #E6162D;
      --zhihu: #0066FF;
      --bilibili: #FB7299;
      --douyin: #ff6a3c;
      --xiaohongshu: #FF2442;
      --rising: #00c853;
      --falling: #ff1744;
      --new: #00b0ff;
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      background: var(--bg);
      color: var(--text);
      font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
      min-height: 100vh;
      padding: 24px;
    }

    .container { max-width: 1200px; margin: 0 auto; }

    /* Header */
    .header {
      text-align: center;
      margin-bottom: 32px;
    }
    .header h1 {
      font-size: 2rem;
      font-weight: 700;
      background: linear-gradient(135deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff, #9b59b6);
      background-size: 300% 300%;
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      animation: gradient 6s ease infinite;
    }
    @keyframes gradient {
      0%, 100% { background-position: 0% 50%; }
      50% { background-position: 100% 50%; }
    }
    .header .subtitle {
      color: var(--text-dim);
      margin-top: 8px;
      font-size: 0.9rem;
    }
    .header .time {
      display: inline-block;
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 20px;
      padding: 4px 16px;
      margin-top: 12px;
      font-size: 0.85rem;
      color: var(--text-dim);
    }

    /* Stats Cards */
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      gap: 12px;
      margin-bottom: 24px;
    }
    .stat-card {
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 16px;
      text-align: center;
      transition: all 0.2s;
    }
    .stat-card:hover {
      transform: translateY(-2px);
      border-color: var(--p-color, var(--border));
    }
    .stat-card .platform-name {
      font-size: 0.85rem;
      color: var(--text-dim);
      margin-bottom: 8px;
    }
    .stat-card .count {
      font-size: 1.8rem;
      font-weight: 700;
      color: var(--p-color, var(--text));
    }
    .stat-card .desc {
      font-size: 0.75rem;
      color: var(--text-dim);
      margin-top: 4px;
    }

    /* Charts Grid */
    .charts-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 16px;
      margin-bottom: 24px;
    }
    .chart-card {
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 20px;
    }
    .chart-card.full-width {
      grid-column: 1 / -1;
    }
    .chart-title {
      font-size: 1rem;
      font-weight: 600;
      margin-bottom: 16px;
      padding-bottom: 10px;
      border-bottom: 1px solid var(--border);
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .chart-title .dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--new);
    }
    .chart-wrap {
      position: relative;
      height: 280px;
    }
    .chart-wrap.bar { height: 260px; }
    .chart-wrap.cross { height: 200px; }

    /* Platform Tables */
    .platform-section {
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 12px;
      margin-bottom: 16px;
      overflow: hidden;
    }
    .platform-header {
      padding: 14px 20px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      border-bottom: 1px solid var(--border);
    }
    .platform-title {
      display: flex;
      align-items: center;
      gap: 10px;
      font-weight: 600;
    }
    .platform-badge {
      display: inline-block;
      width: 10px;
      height: 10px;
      border-radius: 50%;
    }
    .platform-count {
      font-size: 0.8rem;
      color: var(--text-dim);
      background: var(--bg);
      padding: 2px 10px;
      border-radius: 10px;
    }
    .topic-list {
      padding: 0;
    }
    .topic-item {
      display: flex;
      align-items: center;
      padding: 10px 20px;
      border-bottom: 1px solid var(--border);
      transition: background 0.15s;
      gap: 12px;
    }
    .topic-item:last-child { border-bottom: none; }
    .topic-item:hover { background: var(--card-hover); }
    .topic-rank {
      min-width: 28px;
      height: 28px;
      line-height: 28px;
      text-align: center;
      border-radius: 6px;
      font-size: 0.8rem;
      font-weight: 700;
    }
    .rank-top3 { background: linear-gradient(135deg, #ffd700, #ff8c00); color: #000; }
    .rank-normal { background: var(--bg); color: var(--text-dim); }
    .topic-info { flex: 1; min-width: 0; }
    .topic-name {
      font-size: 0.9rem;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .topic-meta {
      font-size: 0.75rem;
      color: var(--text-dim);
      margin-top: 2px;
    }
    .topic-hot {
      font-size: 0.8rem;
      color: var(--text-dim);
      font-weight: 500;
    }
    .topic-trend {
      font-size: 0.75rem;
      padding: 2px 8px;
      border-radius: 4px;
      font-weight: 500;
    }
    .trend-new { background: rgba(0,176,255,0.15); color: var(--new); }
    .trend-rise { background: rgba(0,200,83,0.15); color: var(--rising); }
    .trend-fall { background: rgba(255,23,68,0.15); color: var(--falling); }

    /* Cross Platform */
    .cross-platform-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 12px;
    }
    .cross-item {
      background: var(--bg);
      border: 1px solid var(--border);
      border-radius: 10px;
      padding: 14px;
    }
    .cross-item .topic-name {
      font-size: 0.9rem;
      margin-bottom: 8px;
    }
    .cross-platforms {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
    }
    .cross-p-badge {
      font-size: 0.7rem;
      padding: 2px 8px;
      border-radius: 4px;
      font-weight: 500;
    }

    /* Monitor Alerts */
    .monitor-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
      gap: 12px;
    }
    .monitor-item {
      background: var(--bg);
      border: 1px solid var(--border);
      border-left: 3px solid var(--new);
      border-radius: 8px;
      padding: 12px 14px;
    }
    .monitor-item .keyword {
      font-size: 0.75rem;
      color: var(--new);
      margin-bottom: 4px;
      font-weight: 600;
    }
    .monitor-item .topic {
      font-size: 0.88rem;
      margin-bottom: 4px;
    }
    .monitor-item .meta {
      font-size: 0.75rem;
      color: var(--text-dim);
    }

    /* Section */
    .section-title {
      font-size: 1.1rem;
      font-weight: 600;
      margin: 24px 0 16px;
      padding-left: 12px;
      border-left: 3px solid var(--weibo);
      display: flex;
      align-items: center;
      gap: 8px;
    }

    /* Footer */
    .footer {
      text-align: center;
      margin-top: 32px;
      padding: 16px;
      color: var(--text-dim);
      font-size: 0.8rem;
    }

    /* Responsive */
    @media (max-width: 900px) {
      .stats-grid { grid-template-columns: repeat(3, 1fr); }
      .charts-grid { grid-template-columns: 1fr; }
    }
    @media (max-width: 600px) {
      body { padding: 12px; }
      .stats-grid { grid-template-columns: repeat(2, 1fr); }
    }

    /* Platform specific colors */
    .weibo { --p-color: var(--weibo); }
    .zhihu { --p-color: var(--zhihu); }
    .bilibili { --p-color: var(--bilibili); }
    .douyin { --p-color: var(--douyin); }
    .xiaohongshu { --p-color: var(--xiaohongshu); }
  </style>
</head>
<body>
  <div class="container">

    <!-- Header -->
    <div class="header">
      <h1>🔥 热点雷达</h1>
      <div class="subtitle">全网热点追踪 · 实时可视化报告</div>
      <div class="time">📅 ` + dateStr + ` &nbsp;⏰ ` + timeStr + `</div>
    </div>

    <!-- Stats Overview -->
    <div class="stats-grid">
      <div class="stat-card weibo">
        <div class="platform-name">微博</div>
        <div class="count">` + counts.weibo + `</div>
        <div class="desc">条热榜</div>
      </div>
      <div class="stat-card zhihu">
        <div class="platform-name">知乎</div>
        <div class="count">` + counts.zhihu + `</div>
        <div class="desc">条热榜</div>
      </div>
      <div class="stat-card bilibili">
        <div class="platform-name">B站</div>
        <div class="count">` + counts.bilibili + `</div>
        <div class="desc">条热榜</div>
      </div>
      <div class="stat-card douyin">
        <div class="platform-name">抖音</div>
        <div class="count">` + counts.douyin + `</div>
        <div class="desc">条热榜</div>
      </div>
      <div class="stat-card xiaohongshu">
        <div class="platform-name">小红书</div>
        <div class="count">` + counts.xiaohongshu + `</div>
        <div class="desc">条热榜</div>
      </div>
    </div>

    <!-- Charts -->
    <div class="charts-grid">
      <div class="chart-card full-width">
        <div class="chart-title"><span class="dot"></span>各平台TOP10热度对比</div>
        <div class="chart-wrap bar">
          <canvas id="platformChart"></canvas>
        </div>
      </div>

      <div class="chart-card">
        <div class="chart-title"><span class="dot" style="background:var(--new)"></span>新增话题</div>
        <div class="chart-wrap">
          <canvas id="newChart"></canvas>
        </div>
      </div>

      <div class="chart-card">
        <div class="chart-title"><span class="dot" style="background:var(--rising)"></span>上升话题</div>
        <div class="chart-wrap">
          <canvas id="riseChart"></canvas>
        </div>
      </div>
    </div>

    <!-- Cross Platform -->
    ` + (crossPlatform.length > 0 ? `
    <div class="section-title">🌐 跨平台热点 (同时在多个平台登榜)</div>
    <div class="cross-platform-grid">
      ${crossPlatform.map(item => {
        const platformColors = {
          '微博': 'var(--weibo)', '知乎': 'var(--zhihu)',
          'B站': 'var(--bilibili)', '抖音': 'var(--douyin)', '小红书': 'var(--xiaohongshu)'
        };
        return `<div class="cross-item">
          <div class="topic-name">${escapeHtml(item.topic)}</div>
          <div class="cross-platforms">
            ${item.platforms.map(p => `<span class="cross-p-badge" style="background:${platformColors[p]}22;color:${platformColors[p]}">${p}</span>`).join('')}
          </div>
        </div>`;
      }).join('')}
    </div>
    ` : '') + `

    <!-- Monitor Alerts -->
    ` + (monitorMatches.length > 0 ? `
    <div class="section-title">🔔 话题监控告警</div>
    <div class="monitor-grid">
      ${monitorMatches.map(item => `<div class="monitor-item">
        <div class="keyword">关键词: ${escapeHtml(item.keyword)}</div>
        <div class="topic">${escapeHtml(item.topic)}</div>
        <div class="meta">${item.platform} · 第${item.rank}位 · 热度 ${formatHotValue(item.hotValue)}</div>
      </div>`).join('')}
    </div>
    ` : '') + `

    <!-- Platform Hot Lists -->
    <div class="section-title">📊 各平台热榜详情</div>

    ${generatePlatformSection('weibo', currentData.weibo, '微博', 'var(--weibo)')}
    ${generatePlatformSection('zhihu', currentData.zhihu, '知乎', 'var(--zhihu)')}
    ${generatePlatformSection('bilibili', currentData.bilibili, 'B站', 'var(--bilibili)')}
    ${generatePlatformSection('douyin', currentData.douyin, '抖音', 'var(--douyin)')}
    ${generatePlatformSection('xiaohongshu', currentData.xiaohongshu, '小红书', 'var(--xiaohongshu)')}

    <!-- Footer -->
    <div class="footer">
      Generated by 热点雷达 · ${timestamp}
    </div>
  </div>

  <script>
    const platformData = ` + platformDataJson + `;
    const crossPlatformData = ` + crossPlatformJson + `;
    const trendsData = ` + trendsJson + `;
    const monitorData = ` + monitorJson + `;

    const platformColors = {
      weibo: '#E6162D',
      zhihu: '#0066FF',
      bilibili: '#FB7299',
      douyin: '#ff6a3c',
      xiaohongshu: '#FF2442',
    };
    const platformNames = {
      weibo: '微博', zhihu: '知乎', bilibili: 'B站', douyin: '抖音', xiaohongshu: '小红书'
    };

    // Platform comparison chart (bar)
    (function() {
      const ctx = document.getElementById('platformChart');
      if (!ctx) return;
      const labels = [];
      const datasets = [];

      Object.keys(platformData).forEach(function(p) {
        labels.push(platformNames[p]);
        datasets.push({
          label: platformNames[p],
          data: platformData[p].map(function(item, i) {
            return {
              x: platformNames[p] + '-' + (i+1),
              y: item.hotValue || 0,
              topic: item.topic,
            };
          }),
          backgroundColor: platformColors[p] + '99',
          borderColor: platformColors[p],
          borderWidth: 1,
        });
      });

      new Chart(ctx, {
        type: 'bar',
        data: {
          labels: labels,
          datasets: Object.keys(platformData).map(function(p) {
            return {
              label: platformNames[p],
              data: platformData[p].map(function(item) { return item.hotValue || 0; }),
              backgroundColor: platformColors[p] + 'aa',
              borderColor: platformColors[p],
              borderWidth: 1,
              borderRadius: 4,
            };
          }),
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: {
            legend: { display: false },
            tooltip: {
              callbacks: {
                title: function(ctx) { return ctx[0].label; },
                label: function(ctx) {
                  const d = platformData[Object.keys(platformData)[ctx.datasetIndex]][ctx.dataIndex];
                  return d ? [d.topic, '热度: ' + d.hotValue] : '';
                }
              }
            }
          },
          scales: {
            y: { display: false },
            x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#8888aa' } }
          }
        }
      });
    })();

    // New topics chart
    (function() {
      const ctx = document.getElementById('newChart');
      if (!ctx || trendsData.new.length === 0) return;
      new Chart(ctx, {
        type: 'bar',
        data: {
          labels: trendsData.new.map(function(d) { return truncate(d.topic, 8); }),
          datasets: [{
            label: '新增',
            data: trendsData.new.map(function(d) { return d.hotValue || 0; }),
            backgroundColor: '#00b0ff55',
            borderColor: '#00b0ff',
            borderWidth: 1,
            borderRadius: 4,
          }]
        },
        options: {
          indexAxis: 'y',
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { display: false } },
          scales: {
            x: { display: false },
            y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#8888aa', font: { size: 10 } } }
          }
        }
      });
    })();

    // Rising topics chart
    (function() {
      const ctx = document.getElementById('riseChart');
      if (!ctx || trendsData.rising.length === 0) return;
      new Chart(ctx, {
        type: 'bar',
        data: {
          labels: trendsData.rising.map(function(d) { return truncate(d.topic, 8); }),
          datasets: [{
            label: '上升',
            data: trendsData.rising.map(function(d) { return d.delta || 0; }),
            backgroundColor: '#00c85355',
            borderColor: '#00c853',
            borderWidth: 1,
            borderRadius: 4,
          }]
        },
        options: {
          indexAxis: 'y',
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { display: false } },
          scales: {
            x: { display: false },
            y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#8888aa', font: { size: 10 } } }
          }
        }
      });
    })();

    function truncate(str, len) {
      return str.length > len ? str.substring(0, len) + '...' : str;
    }
  </script>
</body>
</html>`;

  return html;
}

// 生成平台热榜区块HTML
function generatePlatformSection(platformKey, data, platformName, color) {
  if (!data || data.length === 0) {
    return `<div class="platform-section ${platformKey}">
      <div class="platform-header">
        <div class="platform-title">
          <span class="platform-badge" style="background:${color}"></span>
          ${platformName}
        </div>
        <span class="platform-count">数据获取失败</span>
      </div>
    </div>`;
  }

  const items = data.slice(0, 10).map(function(item) {
    const rankClass = item.rank <= 3 ? 'rank-top3' : 'rank-normal';
    const trendLabel = getTrendLabel(item, platformKey);
    const trendClass = getTrendClass(item, platformKey);

    return `<div class="topic-item">
      <span class="topic-rank ${rankClass}">${item.rank}</span>
      <div class="topic-info">
        <div class="topic-name">${escapeHtml(item.topic)}</div>
        ${item.label ? `<div class="topic-meta">${escapeHtml(item.label)}</div>` : ''}
      </div>
      ${trendLabel ? `<span class="topic-trend ${trendClass}">${trendLabel}</span>` : ''}
      <span class="topic-hot">${formatHotValue(item.hotValue)}</span>
    </div>`;
  }).join('');

  return `<div class="platform-section ${platformKey}">
    <div class="platform-header">
      <div class="platform-title">
        <span class="platform-badge" style="background:${color}"></span>
        ${platformName}
      </div>
      <span class="platform-count">${data.length}条</span>
    </div>
    <div class="topic-list">
      ${items}
    </div>
  </div>`;
}

// 获取话题趋势标签
function getTrendLabel(item, platformKey) {
  // 这个函数在HTML中是静态调用的，但数据在JS中
  // 我们通过注入的trendsData来动态显示
  return '';
}

function getTrendClass(item, platformKey) {
  return '';
}

// HTML转义
function escapeHtml(str) {
  if (!str) return '';
  return str.replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// 保存HTML报告
function saveHtmlReport(html, fileName) {
  const today = new Date().toISOString().split('T')[0];
  const reportDir = path.join(CONFIG.dataDir, 'reports');

  if (!fs.existsSync(reportDir)) {
    fs.mkdirSync(reportDir, { recursive: true });
  }

  const filePath = fileName || path.join(reportDir, today + '.html');
  fs.writeFileSync(filePath, html, 'utf-8');
  console.log('HTML Report saved: ' + filePath);

  return filePath;
}

// 主函数
async function main() {
  const today = new Date().toISOString().split('T')[0];
  const snapshotPath = path.join(CONFIG.dataDir, 'trends', today + '.json');

  let currentData;
  if (fs.existsSync(snapshotPath)) {
    currentData = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
    console.log('Loaded today data: ' + snapshotPath);
  } else {
    console.log('Today data not found. Please run collector.js first.');
    process.exit(1);
  }

  const html = generateHtmlReport(currentData);
  const reportPath = saveHtmlReport(html);

  console.log('\nHTML Report generated successfully');
  console.log('File: ' + reportPath);

  return { html: html, reportPath: reportPath };
}

// 导出
module.exports = {
  generateHtmlReport: generateHtmlReport,
  saveHtmlReport: saveHtmlReport,
  formatDate: formatDate,
  formatHotValue: formatHotValue,
  main: main,
};

// 直接运行
if (require.main === module) {
  main().catch(function (e) {
    console.error('HTML report generation failed:', e);
    process.exit(1);
  });
}


path: scripts/index.js
content: /**
 * 热点雷达 - 主入口
 * 一站式全网热榜追踪工具
 */

const path = require('path');
const fs = require('fs');

// 引入子模块
const collector = require('./collector');
const reporter = require('./reporter');
const htmlReporter = require('./htmlReporter');
const monitor = require('./monitor');

// 配置
const CONFIG = {
  scriptsDir: __dirname,
  dataDir: path.join(__dirname, '../data'),
  configDir: path.join(__dirname, '../config'),
};

// 显示Banner
function showBanner() {
  console.log(`
╔═══════════════════════════════════════════════════════════╗
║                                                           ║
║     🌐  热 点 雷 达  -  全网热榜追踪器                     ║
║                                                           ║
║     聚合微博/知乎/抖音/B站/小红书热搜榜单                 ║
║     支持趋势分析、话题监控、定时推送                       ║
║                                                           ║
╚═══════════════════════════════════════════════════════════╝
`);
}

// 显示帮助
function showHelp() {
  console.log(`
📖 使用指南

命令:
  node index.js fetch          采集今日全网热榜数据
  node index.js report         生成今日热点报告
  node index.js html           生成HTML可视化报告（含图表）
  node index.js trend          分析趋势变化（需有历史数据）
  node index.js monitor        话题监控管理
  node index.js quick          快速模式：采集+报告一步完成
  node index.js init           初始化目录结构
  node index.js help           显示帮助

快速开始:
  1. node index.js init        初始化目录
  2. node index.js quick       一键采集并生成报告

示例:
  node index.js fetch          获取最新热榜
  node index.js report         生成Markdown报告
  node index.js monitor add "AI"   添加监控关键词
`);
}

// 初始化目录
function init() {
  console.log('📁 初始化目录结构...');
  collector.ensureDirs ? collector.ensureDirs() : null;

  // 确保配置目录存在
  if (!fs.existsSync(CONFIG.configDir)) {
    fs.mkdirSync(CONFIG.configDir, { recursive: true });
  }

  // 创建默认监控配置
  const monitorConfigPath = path.join(CONFIG.configDir, 'monitor.json');
  if (!fs.existsSync(monitorConfigPath)) {
    fs.writeFileSync(monitorConfigPath, JSON.stringify({ keywords: [], enabled: true }, null, 2), 'utf-8');
    console.log('✅ 已创建默认监控配置');
  }

  // 创建README
  const readmePath = path.join(__dirname, '../README.md');
  if (!fs.existsSync(readmePath)) {
    const readme = `# 🌐 热点雷达 - 全网热榜追踪器

> 聚合微博/知乎/抖音/B站/小红书热搜榜单，支持趋势分析和话题监控

## 功能特性

- ✅ 实时采集五大平台热榜
- ✅ 趋势追踪：对比昨日发现新上榜/落榜话题
- ✅ 跨平台热点：发现同时在多个平台发酵的话题
- ✅ 话题监控：关键词订阅，有动态时及时提醒
- ✅ 每日报告：自动生成Markdown格式热点日报

## 快速开始

\`\`\`bash
# 采集今日热榜
node scripts/index.js fetch

# 生成今日报告
node scripts/index.js report

# 一键完成（采集+报告）
node scripts/index.js quick
\`\`\`

## 目录结构

\`\`\`
hotspot-radar/
├── data/                    # 数据存储
│   ├── history/            # 历史热榜
│   │   ├── weibo/
│   │   ├── zhihu/
│   │   ├── bilibili/
│   │   ├── douyin/
│   │   └── xiaohongshu/
│   ├── trends/             # 每日趋势快照
│   └── reports/           # 生成的报告
├── config/                 # 配置文件
│   └── monitor.json       # 监控关键词配置
└── scripts/                # 脚本文件
    ├── index.js           # 主入口
    ├── collector.js       # 数据采集
    ├── reporter.js        # 报告生成
    └── monitor.js         # 监控管理
\`\`\`

## 话题监控

\`\`\`bash
# 添加监控关键词
node scripts/monitor.js add "人工智能"

# 移除监控关键词
node scripts/monitor.js remove "人工智能"

# 列出所有关键词
node scripts/monitor.js list
\`\`\`

## 数据来源

- 微博热搜
- 知乎热榜
- 抖音热搜
- B站热榜
- 小红书热榜

---
*由「热点雷达」Skill生成*
`;
    fs.writeFileSync(readmePath, readme, 'utf-8');
    console.log('✅ 已创建README.md');
  }

  console.log('\n✅ 初始化完成！');
  console.log('\n下一步:');
  console.log('  1. node index.js quick   # 一键采集+生成报告');
  console.log('  2. node index.js monitor add "关键词"  # 添加监控话题\n');
}

// 快速模式
async function quick() {
  console.log('🚀 快速模式：采集 + 生成报告\n');
  try {
    await collector.main();
    await reporter.main();
    console.log('\n✅ 全部完成！');
  } catch (e) {
    console.error('❌ 执行失败:', e);
    process.exit(1);
  }
}

// 主函数
async function main() {
  showBanner();

  const args = process.argv.slice(2);
  const command = args[0];

  switch (command) {
    case 'fetch':
      try {
        const data = await collector.fetchAll();
        collector.saveHistory(data);
        console.log('\n✅ 数据采集完成');
      } catch (e) {
        console.error('❌ 采集失败:', e);
        process.exit(1);
      }
      break;

    case 'report':
      try {
        await reporter.main();
      } catch (e) {
        console.error('❌ 报告生成失败:', e);
        process.exit(1);
      }
      break;

    case 'html':
      try {
        await htmlReporter.main();
      } catch (e) {
        console.error('❌ HTML报告生成失败:', e);
        process.exit(1);
      }
      break;

    case 'trend':
      console.log('📈 趋势分析模式');
      // TODO: 实现独立趋势分析
      break;

    case 'monitor':
      // 传递给monitor模块
      monitor.main();
      break;

    case 'quick':
      await quick();
      break;

    case 'init':
      init();
      break;

    case 'help':
    default:
      showHelp();
  }
}

// 导出模块接口
module.exports = {
  fetchAll: collector.fetchAll,
  saveHistory: collector.saveHistory,
  generateReport: reporter.generateReport,
  saveReport: reporter.saveReport,
  generateHtmlReport: htmlReporter.generateHtmlReport,
  saveHtmlReport: htmlReporter.saveHtmlReport,
  addMonitor: monitor.addKeyword,
  removeMonitor: monitor.removeKeyword,
  listMonitor: monitor.listKeywords,
};

// 直接运行时执行
if (require.main === module) {
  main();
}


path: scripts/monitor.js
content: /**
 * 热点雷达 - 话题监控配置管理
 */

const fs = require('fs');
const path = require('path');

const CONFIG = {
  configDir: path.join(__dirname, '../config'),
};

// 确保配置目录存在
function ensureConfigDir() {
  if (!fs.existsSync(CONFIG.configDir)) {
    fs.mkdirSync(CONFIG.configDir, { recursive: true });
  }
}

// 默认监控配置
const DEFAULT_MONITOR = {
  keywords: [],
  enabled: true,
  notifyOnNew: true,
  notifyOnTop10: true
};

// 读取监控配置
function loadConfig() {
  ensureConfigDir();
  const configPath = path.join(CONFIG.configDir, 'monitor.json');

  try {
    if (fs.existsSync(configPath)) {
      return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    }
  } catch (e) {
    console.error('读取监控配置失败:', e.message);
  }

  return { ...DEFAULT_MONITOR };
}

// 保存监控配置
function saveConfig(config) {
  ensureConfigDir();
  const configPath = path.join(CONFIG.configDir, 'monitor.json');
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
  console.log('监控配置已保存:', configPath);
}

// 添加关键词
function addKeyword(keyword) {
  const config = loadConfig();
  const normalizedKeyword = keyword.trim();

  if (!normalizedKeyword) {
    console.error('关键词不能为空');
    return false;
  }

  if (config.keywords.includes(normalizedKeyword)) {
    console.log(`关键词 "${normalizedKeyword}" 已存在`);
    return false;
  }

  config.keywords.push(normalizedKeyword);
  saveConfig(config);
  console.log(`已添加监控关键词: ${normalizedKeyword}`);
  return true;
}

// 移除关键词
function removeKeyword(keyword) {
  const config = loadConfig();
  const normalizedKeyword = keyword.trim();
  const index = config.keywords.indexOf(normalizedKeyword);

  if (index === -1) {
    console.log(`关键词 "${normalizedKeyword}" 不存在`);
    return false;
  }

  config.keywords.splice(index, 1);
  saveConfig(config);
  console.log(`已移除监控关键词: ${normalizedKeyword}`);
  return true;
}

// 列出所有关键词
function listKeywords() {
  const config = loadConfig();
  console.log('\n📋 当前监控关键词:');
  if (config.keywords.length === 0) {
    console.log('  (暂无监控关键词)');
  } else {
    config.keywords.forEach((kw, idx) => {
      console.log(`  ${idx + 1}. ${kw}`);
    });
  }
  console.log('');
  return config.keywords;
}

// 启用/禁用监控
function setEnabled(enabled) {
  const config = loadConfig();
  config.enabled = enabled;
  saveConfig(config);
  console.log(`监控已${enabled ? '启用' : '禁用'}`);
  return config;
}

// 显示帮助
function showHelp() {
  console.log(`
🔔 热点雷达 - 话题监控管理

用法:
  node monitor.js add <关键词>    添加监控关键词
  node monitor.js remove <关键词> 移除监控关键词
  node monitor.js list           列出所有监控关键词
  node monitor.js enable         启用监控
  node monitor.js disable        禁用监控
  node monitor.js status         查看监控状态

示例:
  node monitor.js add "人工智能"
  node monitor.js add "考研"
  node monitor.js remove "人工智能"
  node monitor.js list
`);
}

// 主函数
function main() {
  const args = process.argv.slice(2);
  const command = args[0];

  switch (command) {
    case 'add':
      if (!args[1]) {
        console.error('请提供要添加的关键词');
        process.exit(1);
      }
      addKeyword(args.slice(1).join(' '));
      break;

    case 'remove':
      if (!args[1]) {
        console.error('请提供要移除的关键词');
        process.exit(1);
      }
      removeKeyword(args.slice(1).join(' '));
      break;

    case 'list':
      listKeywords();
      break;

    case 'enable':
      setEnabled(true);
      break;

    case 'disable':
      setEnabled(false);
      break;

    case 'status':
      const config = loadConfig();
      console.log('\n🔔 监控状态:', config.enabled ? '已启用' : '已禁用');
      console.log('📋 监控关键词:');
      if (config.keywords.length === 0) {
        console.log('  (暂无)');
      } else {
        config.keywords.forEach(kw => console.log(`  - ${kw}`));
      }
      console.log('');
      break;

    default:
      showHelp();
  }
}

module.exports = { addKeyword, removeKeyword, listKeywords, setEnabled, loadConfig, saveConfig };

// 直接运行时执行
if (require.main === module) {
  main();
}


path: scripts/reporter.js
content: /**
 * 热点雷达 - 报告生成器
 * 生成Markdown格式的全网热点日报
 */

const fs = require('fs');
const path = require('path');

const CONFIG = {
  dataDir: path.join(__dirname, '../data'),
  historyDir: path.join(__dirname, '../data/history'),
  configDir: path.join(__dirname, '../config'),
};

// 格式化日期
function formatDate(date = new Date()) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return year + '年' + month + '月' + day + '日';
}

// 格式化时间
function formatTime(date = new Date()) {
  const h = String(date.getHours()).padStart(2, '0');
  const m = String(date.getMinutes()).padStart(2, '0');
  const s = String(date.getSeconds()).padStart(2, '0');
  return h + ':' + m + ':' + s;
}

// 读取历史数据
function loadHistory(platform, daysAgo = 1) {
  const date = new Date();
  date.setDate(date.getDate() - daysAgo);
  const fileName = date.toISOString().split('T')[0] + '.json';
  const filePath = path.join(CONFIG.historyDir, platform, fileName);

  try {
    if (fs.existsSync(filePath)) {
      return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
    }
  } catch (e) {
    console.error('Read history failed [' + platform + '] [' + daysAgo + 'days ago]:', e.message);
  }
  return [];
}

// 读取监控配置
function loadMonitorConfig() {
  const configPath = path.join(CONFIG.configDir, 'monitor.json');
  try {
    if (fs.existsSync(configPath)) {
      return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
    }
  } catch (e) {
    console.error('Read monitor config failed:', e.message);
  }
  return { keywords: [] };
}

// 生成平台热榜表格
function generatePlatformTable(platform, data, platformName) {
  if (!data || data.length === 0) {
    return '### ' + platformName + '\n> Data fetch failed\n';
  }

  let table = '### ' + platformName + '\n\n';
  table += '| Rank | Topic | HotValue | Label |\n';
  table += '|------|-------|----------|-------|\n';

  data.slice(0, 20).forEach(function(item) {
    table += '| ' + item.rank + ' | ' + item.topic + ' | ' + formatHotValue(item.hotValue) + ' | ' + (item.label || '-') + ' |\n';
  });

  return table;
}

// 格式化热度值
function formatHotValue(value) {
  if (value >= 100000000) {
    return (value / 100000000).toFixed(1) + 'yi';
  } else if (value >= 10000) {
    return (value / 10000).toFixed(1) + 'wan';
  }
  return value.toString();
}

// 趋势分析
function analyzeTrends(currentData) {
  const platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];
  const platformNames = { weibo: 'Weibo', zhihu: 'Zhihu', bilibili: 'Bilibili', douyin: 'Douyin', xiaohongshu: 'XHS' };

  const trends = {
    newTopics: [],
    risingTopics: [],
    fallingTopics: [],
    outTopics: []
  };

  platforms.forEach(function(platform) {
    const current = currentData[platform] || [];
    const yesterday = loadHistory(platform, 1);

    if (yesterday.length === 0) return;

    const yesterdayTopics = new Map(yesterday.map(function(item) { return [item.topic, item.rank]; }));

    current.forEach(function(item) {
      const prevRank = yesterdayTopics.get(item.topic);
      if (prevRank === undefined) {
        trends.newTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          currentRank: item.rank
        });
      } else if (item.rank < prevRank) {
        trends.risingTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          fromRank: prevRank,
          toRank: item.rank
        });
      } else if (item.rank > prevRank) {
        trends.fallingTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          fromRank: prevRank,
          toRank: item.rank
        });
      }
    });

    yesterday.forEach(function(item) {
      const stillExists = current.some(function(c) { return c.topic === item.topic; });
      if (!stillExists && item.rank <= 10) {
        trends.outTopics.push({
          topic: item.topic,
          platform: platformNames[platform],
          prevRank: item.rank
        });
      }
    });
  });

  return trends;
}

// 跨平台热点分析
function findCrossPlatformTopics(currentData) {
  const platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];
  const platformNames = { weibo: 'Weibo', zhihu: 'Zhihu', bilibili: 'Bilibili', douyin: 'Douyin', xiaohongshu: 'XHS' };

  const topicMap = new Map();

  platforms.forEach(function(platform) {
    const data = currentData[platform] || [];
    data.slice(0, 20).forEach(function(item) {
      const normalizedTopic = item.topic.toLowerCase().trim();
      if (!topicMap.has(normalizedTopic)) {
        topicMap.set(normalizedTopic, { topic: item.topic, platforms: [] });
      }
      topicMap.get(normalizedTopic).platforms.push(platformNames[platform]);
    });
  });

  const crossPlatform = [];
  topicMap.forEach(function(value) {
    if (value.platforms.length >= 2) {
      crossPlatform.push(value);
    }
  });

  crossPlatform.sort(function(a, b) { return b.platforms.length - a.platforms.length; });

  return crossPlatform.slice(0, 10);
}

// 话题监控检查
function checkMonitorKeywords(currentData) {
  const config = loadMonitorConfig();
  if (!config.keywords || config.keywords.length === 0) {
    return [];
  }

  const platforms = ['weibo', 'zhihu', 'bilibili', 'douyin', 'xiaohongshu'];
  const platformNames = { weibo: 'Weibo', zhihu: 'Zhihu', bilibili: 'Bilibili', douyin: 'Douyin', xiaohongshu: 'XHS' };
  const matches = [];

  platforms.forEach(function(platform) {
    const data = currentData[platform] || [];
    data.forEach(function(item) {
      const topicLower = item.topic.toLowerCase();
      config.keywords.forEach(function(keyword) {
        if (topicLower.includes(keyword.toLowerCase())) {
          matches.push({
            keyword: keyword,
            topic: item.topic,
            platform: platformNames[platform],
            rank: item.rank
          });
        }
      });
    });
  });

  return matches;
}

// 生成Markdown报告
function generateReport(currentData) {
  const today = new Date();
  const dateStr = formatDate(today);
  const timeStr = formatTime(today);

  const totalTopics = {
    weibo: currentData.weibo ? currentData.weibo.length : 0,
    zhihu: currentData.zhihu ? currentData.zhihu.length : 0,
    bilibili: currentData.bilibili ? currentData.bilibili.length : 0,
    douyin: currentData.douyin ? currentData.douyin.length : 0,
    xiaohongshu: currentData.xiaohongshu ? currentData.xiaohongshu.length : 0
  };

  const trends = analyzeTrends(currentData);
  const crossPlatform = findCrossPlatformTopics(currentData);
  const monitorMatches = checkMonitorKeywords(currentData);

  let report = '# [HOT] Daily Hotspot Report | ' + dateStr + '\n\n';
  report += '> Generated: ' + dateStr + ' ' + timeStr + '\n\n';
  report += '## Stats Summary\n\n';
  report += '| Platform | Count | Status |\n';
  report += '|----------|-------|--------|\n';
  report += '| Weibo | ' + totalTopics.weibo + ' | ' + (totalTopics.weibo > 0 ? 'OK' : 'Failed') + ' |\n';
  report += '| Zhihu | ' + totalTopics.zhihu + ' | ' + (totalTopics.zhihu > 0 ? 'OK' : 'Failed') + ' |\n';
  report += '| Bilibili | ' + totalTopics.bilibili + ' | ' + (totalTopics.bilibili > 0 ? 'OK' : 'Failed') + ' |\n';
  report += '| Douyin | ' + totalTopics.douyin + ' | ' + (totalTopics.douyin > 0 ? 'OK' : 'Failed') + ' |\n';
  report += '| Xiaohongshu | ' + totalTopics.xiaohongshu + ' | ' + (totalTopics.xiaohongshu > 0 ? 'OK' : 'Failed') + ' |\n\n';

  if (crossPlatform.length > 0) {
    report += '## Cross-Platform Hot Topics\n\n';
    crossPlatform.slice(0, 10).forEach(function(item, idx) {
      report += (idx + 1) + '. **' + item.topic + '** - Appears on ' + item.platforms.join(', ') + '\n';
    });
    report += '\n';
  }

  report += '## Platform Hot Lists\n\n';
  report += generatePlatformTable('weibo', currentData.weibo, 'Weibo Trending');
  report += '\n';
  report += generatePlatformTable('zhihu', currentData.zhihu, 'Zhihu Hot');
  report += '\n';
  report += generatePlatformTable('bilibili', currentData.bilibili, 'Bilibili Trending');
  report += '\n';
  report += generatePlatformTable('douyin', currentData.douyin, 'Douyin Trending');
  report += '\n';
  report += generatePlatformTable('xiaohongshu', currentData.xiaohongshu, 'Xiaohongshu Trending');
  report += '\n';

  report += '## Trend Analysis\n\n';

  if (trends.newTopics.length > 0) {
    report += '### NEW Entries\n\n';
    trends.newTopics.slice(0, 5).forEach(function(item) {
      report += '- **' + item.topic + '** - First time on list (' + item.platform + ' #' + item.currentRank + ')\n';
    });
    report += '\n';
  }

  if (trends.risingTopics.length > 0) {
    report += '### Rising\n\n';
    trends.risingTopics.slice(0, 5).forEach(function(item) {
      report += '- **' + item.topic + '** - #' + item.fromRank + ' -> #' + item.toRank + ' (' + item.platform + ')\n';
    });
    report += '\n';
  }

  if (trends.fallingTopics.length > 0) {
    report += '### Falling\n\n';
    trends.fallingTopics.slice(0, 5).forEach(function(item) {
      report += '- **' + item.topic + '** - #' + item.fromRank + ' -> #' + item.toRank + ' (' + item.platform + ')\n';
    });
    report += '\n';
  }

  if (trends.outTopics.length > 0) {
    report += '### Dropped Out\n\n';
    trends.outTopics.slice(0, 5).forEach(function(item) {
      report += '- **' + item.topic + '** - Was #' + item.prevRank + ', now out (' + item.platform + ')\n';
    });
    report += '\n';
  }

  if (monitorMatches.length > 0) {
    report += '## Monitor Alerts\n\n';
    report += '| Keyword | Topic | Platform | Rank |\n';
    report += '|---------|-------|----------|------|\n';
    monitorMatches.forEach(function(match) {
      report += '| ' + match.keyword + ' | ' + match.topic + ' | ' + match.platform + ' | #' + match.rank + ' |\n';
    });
    report += '\n';
  }

  report += '---\n\n';
  report += '*Generated by Hotspot Radar*\n';

  return report;
}

// 保存报告
function saveReport(report, fileName) {
  const today = new Date().toISOString().split('T')[0];
  const reportDir = path.join(CONFIG.dataDir, 'reports');

  if (!fs.existsSync(reportDir)) {
    fs.mkdirSync(reportDir, { recursive: true });
  }

  const filePath = fileName || path.join(reportDir, today + '.md');
  fs.writeFileSync(filePath, report, 'utf-8');
  console.log('Report saved: ' + filePath);

  return filePath;
}

// 主函数
async function main() {
  const today = new Date().toISOString().split('T')[0];
  const snapshotPath = path.join(CONFIG.dataDir, 'trends', today + '.json');

  let currentData;
  if (fs.existsSync(snapshotPath)) {
    currentData = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
    console.log('Loaded today data: ' + snapshotPath);
  } else {
    console.log('Today data not found. Please run collector.js first.');
    process.exit(1);
  }

  const report = generateReport(currentData);
  const reportPath = saveReport(report);

  console.log('\nReport generated successfully');
  console.log(report);

  return { report: report, reportPath: reportPath };
}

module.exports = { generateReport: generateReport, saveReport: saveReport, formatDate: formatDate, loadMonitorConfig: loadMonitorConfig };

// Run directly
if (require.main === module) {
  main().catch(function(e) {
    console.error('Report generation failed:', e);
    process.exit(1);
  });
}


path: config/monitor.json
content: {
  "keywords": [
    "人工智能"
  ],
  "enabled": true,
  "notifyOnNew": true,
  "notifyOnTop10": true
}

path: config/push.json
content: {
  "enabled": false,
  "schedule": {
    "morning": "09:00",
    "evening": "21:00"
  },
  "feishu": {
    "webhook": "",
    "enabled": false
  },
  "email": {
    "enabled": false,
    "smtp": "",
    "to": []
  }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)