Skill: noah-1106/toutiao-hot (version 1.0.0)

--- name: toutiao-news-trends description: | 获取今日头条(www.toutiao.com)新闻热榜/热搜榜数据 | Get Toutiao (www.toutiao.com) news hot list/trending data 包含时政要闻、财经、社会事件、国际新闻、科技发展及娱乐八卦等多领域的热门中文资讯 | Includes politics, finance, social events, international news, tech, entertainment and more 输出热点标题、热度值与跳转链接 | Output hot titles, heat values and jump links --- # 今日头条新闻热榜 | Toutiao News Hot List ## 技能概述 | Skill Overview 此技能用于抓取今日头条 PC 端热榜（hot-board）数据，包括： This skill fetches Toutiao PC hot-board data, including: - 热点标题 / Hot topic titles - 热度值（HotValue）/ Heat values - 详情跳转链接（去除冗余查询参数，便于分享）/ Detail links (cleaned for sharing) - 封面图（如有）/ Cover images (if available) - 标签（如"热门事件"等）/ Labels (e.g., "hot event") 数据来源：今日头条 (www.toutiao.com) Data source: Toutiao (www.toutiao.com) --- ## 获取热榜 | Get Hot List 获取热榜（默认 50 条，按榜单顺序返回）： Get hot list (default 50 items, in list order): ```bash node scripts/toutiao.js hot ``` 获取热榜前 N 条： Get top N items: ```bash node scripts/toutiao.js hot 10 ``` --- ## 返回数据字段说明 | Return Data Fields | 字段 / Field | 类型 / Type | 说明 / Description | |-------------|------------|-------------------| | rank | number | 榜单排名（从 1 开始）| List ranking (starting from 1) | | title | string | 热点标题 | Hot topic title | | popularity | number | 热度值（HotValue，已转为数字；解析失败时为 0）| Heat value (parsed to number; 0 if failed) | | link | string | 热点详情链接（已清理 query/hash）| Detail link (cleaned query/hash) | | cover | string \| null | 封面图 URL（如有）| Cover image URL (if available) | | label | string \| null | 标签/标识（如有）| Label (if available) | | clusterId | string | 聚合 ID（字符串化）| Cluster ID (as string) | | categories | string[] | 兴趣分类（如有）| Interest categories (if available) | --- ## 注意事项 | Notes - 该接口为网页端公开接口，返回结构可能变动；若字段缺失可适当容错 This interface is a public web interface; structure may change; handle missing fields gracefully - 访问频繁可能触发风控，脚本内置随机 User-Agent 与超时控制 Frequent access may trigger rate limiting; script includes random User-Agent and timeout control --- ## 数据采集与持久化 | Data Collection & Persistence 新增数据库存储和可视化功能，用于调研数据收集。 New database storage and visualization features for research data collection. ### 快速开始 | Quick Start ```bash # 1. 初始化数据库（首次使用）/ Initialize database (first time) cd scripts python3 init_db.py # 2. 采集数据并保存到数据库 / Collect data and save to database python3 save_to_db.py 50 # 3. 查询数据 / Query data python3 query.py today # 4. 生成 HTML 报告 / Generate HTML report python3 generate_html.py open ../data/index.html ``` ### 新增脚本说明 | New Scripts | 脚本 / Script | 功能 / Function | |--------------|-----------------| | `init_db.py` | 初始化 SQLite 数据库 / Initialize SQLite database | | `save_to_db.py` | 采集热榜并保存到数据库 / Collect hot list and save to DB | | `query.py` | 查询数据库内容 / Query database content | | `generate_html.py` | 生成可视化 HTML 报告 / Generate visual HTML report | ### 使用示例 | Usage Examples ```bash # 采集50条热榜 / Collect 50 hot items cd scripts python3 save_to_db.py 50 # 查看今天的热榜 / View today's hot items python3 query.py today # 查看统计 / View statistics python3 query.py stats 7 ``` ### 数据存储位置 | Data Storage Location ``` data/ ├── toutiao.db # SQLite 数据库 / Database └── index.html # HTML 报告（生成后）/ HTML report (generated) ``` --- ## 致谢 感谢原作者@爱海贼的无处不在 的原版技能toutiao-news-trends开源，本技能基于原版技能进行强化和更新制作而成。

path: data/index.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>今日头条热榜 - 数据可视化</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
            background: #f5f5f5;
            line-height: 1.6;
        }
        
        .header {
            background: linear-gradient(135deg, #ed4040 0%, #c62828 100%);
            color: white;
            padding: 25px 20px;
            position: sticky;
            top: 0;
            z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .header h1 { font-size: 24px; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; }
        .header h1::before { content: "📰"; }
        .header .meta { font-size: 13px; opacity: 0.9; }
        
        .filters {
            background: white;
            padding: 20px;
            border-bottom: 1px solid #e8e8e8;
            display: flex;
            gap: 15px;
            flex-wrap: wrap;
            align-items: flex-end;
        }
        
        .filter-group { display: flex; flex-direction: column; gap: 6px; }
        .filter-group label { font-size: 12px; color: #666; font-weight: 500; }
        .filter-group select, .filter-group input {
            padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;
            font-size: 14px; background: white; min-width: 120px;
        }
        
        .btn { padding: 8px 16px; border: none; border-radius: 6px;
            cursor: pointer; font-size: 14px; background: #f0f0f0; color: #666; }
        .btn:hover { background: #e0e0e0; }
        
        .stats {
            background: #fff0f0;
            padding: 12px 20px;
            font-size: 13px;
            color: #666;
            border-bottom: 1px solid #e8e8e8;
            display: flex;
            justify-content: space-between;
        }
        .stats .highlight { color: #ed4040; font-weight: 600; }
        
        .content { max-width: 900px; margin: 0 auto; padding: 20px; }
        
        .hot-list { display: flex; flex-direction: column; gap: 12px; }
        
        .hot-item {
            display: flex;
            align-items: flex-start;
            padding: 16px 20px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.06);
            text-decoration: none;
            color: inherit;
            transition: transform 0.1s, box-shadow 0.1s;
        }
        
        .hot-item:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }
        
        .rank {
            width: 36px;
            height: 36px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: 700;
            font-size: 16px;
            margin-right: 16px;
            flex-shrink: 0;
        }
        
        .rank.top1 { background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; }
        .rank.top2 { background: linear-gradient(135deg, #ffa94d, #ff8c42); color: white; }
        .rank.top3 { background: linear-gradient(135deg, #ffd43b, #fcc419); color: #333; }
        .rank.normal { background: #f0f0f0; color: #666; }
        
        .item-content { flex: 1; min-width: 0; }
        
        .item-title {
            font-size: 15px;
            color: #1a1a1a;
            margin-bottom: 8px;
            line-height: 1.5;
            word-break: break-word;
        }
        
        .item-meta { display: flex; align-items: center; gap: 12px; font-size: 12px; }
        
        .label-badge {
            padding: 2px 10px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: 600;
            background: #ed4040;
            color: white;
        }
        
        .heat-badge {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            color: #ed4040;
            font-weight: 500;
        }
        
        .empty { text-align: center; padding: 80px 20px; color: #999; }
        .empty-icon { font-size: 48px; margin-bottom: 16px; }
        
        @media (max-width: 640px) {
            .content { padding: 12px; }
            .hot-item { padding: 12px 15px; }
            .rank { width: 32px; height: 32px; font-size: 14px; margin-right: 12px; }
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>今日头条热榜数据可视化</h1>
        <div class="meta">
            更新时间: 2026-03-16 05:19 | 
            共 10 条记录
        </div>
    </div>
    
    <div class="filters">
        <div class="filter-group">
            <label>日期</label>
            <select id="dateSelect">
                <option value="">全部</option>
                <option value="2026-03-15">2026-03-15</option>
            </select>
        </div>
        
        <div class="filter-group">
            <label>搜索</label>
            <input type="text" id="searchInput" placeholder="输入关键词...">
        </div>
        
        <button class="btn" onclick="resetFilters()">重置</button>
    </div>
    
    <div class="stats">
        <span>显示 <span class="highlight" id="showCount">10</span> / 10 条</span>
    </div>
    
    <div class="content">
        <div class="hot-list" id="hotList"></div>
    </div>

    <script>
        const items = [{"rank": 1, "title": "市监总局彻查315晚会曝光违法行为", "popularity": 30164001, "link": "https://www.toutiao.com/trending/7617486592181014062/", "label": "热门事件", "cover": "https://p3-sign.toutiaoimg.com/top-static-files-outer/breaknews/399c4877-eb34-4e40-8eec-a62713cfb0cf~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=%2BRn9CRZxXJE9RqR7lXvX%2FEd0XT0%3D", "date": "2026-03-15"}, {"rank": 2, "title": "央视“3·15”晚会点名金山制药", "popularity": 27293517, "link": "https://www.toutiao.com/trending/7617469624271618054/", "label": "热门事件", "cover": "https://p3-sign.toutiaoimg.com/mosaic-legacy/2b29000036f8405561443~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=ujmCqTyJ7JVWvsy1fN7Bn6a8T7M%3D", "date": "2026-03-15"}, {"rank": 3, "title": "中国盾构机全球市场占有率第一", "popularity": 24696195, "link": "https://www.toutiao.com/article/7617386839708418595", "label": "", "cover": "https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/63299744c7c34427b603a630dc949025~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=N86Wqsx1osaWeR1EeheuDqBRF8E%3D", "date": "2026-03-15"}, {"rank": 4, "title": "老师生娃 班里四个男生惊喜探望", "popularity": 22346041, "link": "https://www.toutiao.com/trending/7616113381275435050/", "label": "", "cover": "https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/72182d122d3b4895b49892b243dea01e~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=m4NZ49%2BX9K0NhGOYJXkYg9Qs7tk%3D", "date": "2026-03-15"}, {"rank": 5, "title": "美宜佳被查获非法卷烟139.99万支", "popularity": 20219534, "link": "https://www.toutiao.com/trending/7617370414004813860/", "label": "新进展", "cover": "https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/e7da58296deb417da4fbe753fc782178~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=1MWTBq2hIhsXw7UXJaz74LevX6o%3D", "date": "2026-03-15"}, {"rank": 6, "title": "网警提醒看不见的虚假更要防", "popularity": 18295391, "link": "https://www.toutiao.com/trending/7617333756785410058/", "label": "", "cover": "https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/c331e1d58420440eb7856505df2b54bb~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=BzqxPfgPKMEMnxURDILoC6hbt3c%3D", "date": "2026-03-15"}, {"rank": 7, "title": "伊朗导弹碎片击中美驻以色列领事官邸", "popularity": 16554354, "link": "https://www.toutiao.com/trending/7617447865123786286/", "label": "", "cover": "https://p3-sign.toutiaoimg.com/top-static-files-outer/breaknews/2c808ed8-9902-4fd3-b0f0-ba32bdb26d0f~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=awPTBO68tXvFVzv69Q3jo%2BkBaIY%3D", "date": "2026-03-15"}, {"rank": 8, "title": "315曝光漂白鸡爪 生产车间令人作呕", "popularity": 14978999, "link": "https://www.toutiao.com/trending/7617383931079491590/", "label": "热门事件", "cover": "https://p3-sign.toutiaoimg.com/top-static-files-outer/breaknews/f7f193df-6377-4950-a828-e22ea5ca5f53~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=svSpACl%2BnOgQ2aGGO9dXWKK42%2Fw%3D", "date": "2026-03-15"}, {"rank": 9, "title": "胖东来眼镜自选超市人山人海", "popularity": 13553559, "link": "https://www.toutiao.com/trending/7616305988971298822/", "label": "", "cover": "https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/e6d84e9e18314d26a9c4cf7c5386214c~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=ZiLzQRFq3xwsrgTOFagzvAlaEDI%3D", "date": "2026-03-15"}, {"rank": 10, "title": "卧底199元桂林6日游3天被排7次购物", "popularity": 12263767, "link": "https://www.toutiao.com/trending/7616999503977627689/", "label": "", "cover": "https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/a52a9e3647bf44b0872a0d4037b5889f~tplv-tt-shrink:960:540.jpeg?_iz=30575&from=sign_default&lk3s=8d617dac&x-expires=1776124800&x-signature=NgbSaELSoq0HshlEsSARHvMXch8%3D", "date": "2026-03-15"}];
        
        function formatHeat(num) {
            if (num >= 10000) return (num / 10000).toFixed(1) + '万';
            return num.toLocaleString();
        }
        
        function getRankClass(rank) {
            if (rank === 1) return 'top1';
            if (rank === 2) return 'top2';
            if (rank === 3) return 'top3';
            return 'normal';
        }
        
        function renderItems() {
            const dateSelect = document.getElementById('dateSelect').value;
            const search = document.getElementById('searchInput').value.toLowerCase().trim();
            
            let filtered = items.filter(item => {
                if (dateSelect && item.date !== dateSelect) return false;
                if (search && !item.title.toLowerCase().includes(search)) return false;
                return true;
            });
            
            document.getElementById('showCount').textContent = filtered.length;
            
            const container = document.getElementById('hotList');
            if (filtered.length === 0) {
                container.innerHTML = `
                    <div class="empty" style="grid-column: 1/-1;">
                        <div class="empty-icon">📰</div>
                        <div>没有找到记录</div>
                    </div>
                `;
                return;
            }
            
            // 按日期分组
            const grouped = {};
            filtered.forEach(item => {
                if (!grouped[item.date]) grouped[item.date] = [];
                grouped[item.date].push(item);
            });
            
            let html = '';
            Object.keys(grouped).sort().reverse().forEach(date => {
                html += grouped[date].map(item => `
                    <a href="${item.link}" class="hot-item" target="_blank">
                        <div class="rank ${getRankClass(item.rank)}">${item.rank}</div>
                        <div class="item-content">
                            <div class="item-title">${item.title}</div>
                            <div class="item-meta">
                                ${item.label ? `<span class="label-badge">${item.label}</span>` : ''}
                                ${item.popularity ? `<span class="heat-badge">🔥 ${formatHeat(item.popularity)}</span>` : ''}
                                <span style="color: #999;">${item.date}</span>
                            </div>
                        </div>
                    </a>
                `).join('');
            });
            
            container.innerHTML = html;
        }
        
        function resetFilters() {
            document.getElementById('dateSelect').value = '';
            document.getElementById('searchInput').value = '';
            renderItems();
        }
        
        document.getElementById('dateSelect').addEventListener('change', renderItems);
        document.getElementById('searchInput').addEventListener('input', renderItems);
        
        renderItems();
    </script>
</body>
</html>

path: scripts/db.py
content: #!/usr/bin/env python3
"""
Toutiao Hot - Database Operations
今日头条热榜数据库操作模块
"""

import sqlite3
import json
import hashlib
from datetime import datetime, timezone
from pathlib import Path

DB_PATH = Path(__file__).parent.parent / "data" / "toutiao.db"

def get_db():
    """获取数据库连接"""
    return sqlite3.connect(DB_PATH)

def generate_id(cluster_id, date_str):
    """生成条目ID"""
    return hashlib.sha256((cluster_id + date_str).encode()).hexdigest()[:16]

def get_today_str():
    """获取今天的日期字符串"""
    return datetime.now(timezone.utc).strftime('%Y-%m-%d')

def save_hot_items(items):
    """
    保存热榜条目到数据库
    
    Args:
        items: 条目列表
    
    Returns:
        dict: {found, new, errors}
    """
    if not items:
        return {'found': 0, 'new': 0, 'errors': 0}
    
    conn = get_db()
    cursor = conn.cursor()
    
    today = get_today_str()
    now = int(datetime.now(timezone.utc).timestamp())
    
    found = len(items)
    new_count = 0
    errors = 0
    
    for item in items:
        try:
            cluster_id = item.get('clusterId', '')
            if not cluster_id:
                continue
            
            # 生成唯一ID
            item_id = generate_id(cluster_id, today)
            rank = item.get('rank', 0)
            title = item.get('title', '')
            popularity = item.get('popularity', 0)
            link = item.get('link', '')
            cover = item.get('cover', '')
            label = item.get('label', '')
            categories = item.get('categories', [])
            categories_json = json.dumps(categories, ensure_ascii=False)
            
            # 检查是否已存在
            cursor.execute('SELECT id FROM hot_items WHERE id = ?', (item_id,))
            if cursor.fetchone():
                continue  # 已存在，跳过
            
            # 插入新记录
            cursor.execute('''
                INSERT INTO hot_items 
                (id, platform, rank, title, popularity, link, cover, label, cluster_id, categories, fetched_at, fetch_date, raw_data)
                VALUES (?, 'toutiao', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (item_id, rank, title, popularity, link, cover, label, cluster_id, categories_json, now, today, json.dumps(item, ensure_ascii=False)))
            
            new_count += 1
            
        except Exception as e:
            print(f"   ⚠️ 保存失败: {e}")
            errors += 1
            continue
    
    conn.commit()
    conn.close()
    
    return {
        'found': found,
        'new': new_count,
        'errors': errors
    }

def log_fetch(found, new, status=0, error=None):
    """记录抓取日志"""
    conn = get_db()
    cursor = conn.cursor()
    
    now = int(datetime.now(timezone.utc).timestamp())
    
    cursor.execute('''
        INSERT INTO fetch_logs 
        (started_at, ended_at, found, new, status, error)
        VALUES (?, ?, ?, ?, ?, ?)
    ''', (now, now, found, new, status, error))
    
    conn.commit()
    conn.close()

def get_items_by_date(date_str=None, limit=100):
    """获取指定日期的热榜条目"""
    if date_str is None:
        date_str = get_today_str()
    
    conn = get_db()
    cursor = conn.cursor()
    
    cursor.execute('''
        SELECT rank, title, popularity, link, label, cover
        FROM hot_items
        WHERE fetch_date = ?
        ORDER BY rank ASC
        LIMIT ?
    ''', (date_str, limit))
    
    rows = cursor.fetchall()
    conn.close()
    
    return [{
        'rank': row[0],
        'title': row[1],
        'popularity': row[2],
        'link': row[3],
        'label': row[4],
        'cover': row[5]
    } for row in rows]

def get_stats(days=7):
    """获取最近N天的统计信息"""
    conn = get_db()
    cursor = conn.cursor()
    
    cursor.execute('''
        SELECT fetch_date, COUNT(*) as count, 
               SUM(popularity) as total_popularity
        FROM hot_items
        WHERE fetch_date >= date('now', '-{} days')
        GROUP BY fetch_date
        ORDER BY fetch_date DESC
    '''.format(days))
    
    rows = cursor.fetchall()
    conn.close()
    
    return [{
        'date': row[0],
        'count': row[1],
        'total_popularity': row[2]
    } for row in rows]

if __name__ == '__main__':
    print(f"Database path: {DB_PATH}")


path: scripts/fetch-toutiao.py
content: #!/usr/bin/env python3
"""
今日头条热榜抓取脚本
调用 toutiao-news-trends-0 技能获取热榜数据
"""

import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

def fetch_toutiao_hot(limit=50):
    """抓取今日头条热榜"""
    print(f"🔥 抓取今日头条热榜 (Top {limit})...")
    
    skill_path = Path.home() / '.openclaw/workspace-tanluzhe/skills/toutiao-news-trends-0'
    script_path = skill_path / 'scripts/toutiao.js'
    
    cmd = ['node', str(script_path), 'hot', str(limit)]
    
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            data = json.loads(result.stdout)
            print(f"   ✅ 抓取到 {len(data)} 条热榜")
            return data
        else:
            print(f"   ❌ 错误: {result.stderr}")
            return []
    except Exception as e:
        print(f"   ❌ 异常: {e}")
        return []

def main():
    if len(sys.argv) < 3:
        print("Usage: python3 fetch-toutiao.py <date_str> <reports_dir>")
        sys.exit(1)
    
    date_str = sys.argv[1]
    reports_dir = sys.argv[2]
    
    print("=" * 60)
    print(f"📰 今日头条热榜抓取 - {date_str}")
    print("=" * 60)
    
    # 抓取数据
    hot_items = fetch_toutiao_hot(50)
    
    if not hot_items:
        print("❌ 抓取失败")
        sys.exit(1)
    
    # 格式化输出
    output = {
        "source": "toutiao-hot",
        "fetch_time": datetime.now(timezone.utc).isoformat(),
        "channel": "hot",
        "total_items": len(hot_items),
        "items": hot_items,
        "metadata": {
            "data_type": "hot_news_ranking",
            "value_for_research": "high"
        }
    }
    
    # 保存文件
    output_file = Path(reports_dir) / f'toutiao-hot-{date_str}.json'
    output_file.parent.mkdir(parents=True, exist_ok=True)
    
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(output, f, indent=2, ensure_ascii=False)
    
    print(f"\n✅ 已保存: {output_file}")
    print(f"📊 共 {len(hot_items)} 条热榜")
    
    # 显示前5条
    print("\n🔥 Top 5:")
    for item in hot_items[:5]:
        label = f"[{item.get('label')}] " if item.get('label') else ""
        print(f"  {item['rank']}. {label}{item['title']}")
        print(f"     热度: {item.get('popularity', 0):,}")
    
    print()

if __name__ == '__main__':
    main()


path: scripts/generate_html.py
content: #!/usr/bin/env python3
"""
Toutiao Hot - Generate HTML Report
今日头条热榜HTML可视化报告
"""

import sqlite3
import json
from datetime import datetime, timezone
from pathlib import Path

DB_PATH = Path(__file__).parent.parent / "data" / "toutiao.db"
OUTPUT_PATH = Path(__file__).parent.parent / "data" / "index.html"

def generate_html():
    """生成HTML报告"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    
    # 获取所有数据
    cursor.execute('''
        SELECT rank, title, popularity, link, label, cover, fetch_date
        FROM hot_items
        ORDER BY fetch_date DESC, rank ASC
        LIMIT 1000
    ''')
    rows = cursor.fetchall()
    
    # 获取所有日期
    cursor.execute('SELECT DISTINCT fetch_date FROM hot_items ORDER BY fetch_date DESC')
    all_dates = [row[0] for row in cursor.fetchall()]
    
    conn.close()
    
    if not rows:
        print("❌ 数据库中没有数据")
        return
    
    # 转换数据
    items = []
    for row in rows:
        items.append({
            'rank': row['rank'],
            'title': row['title'],
            'popularity': row['popularity'] or 0,
            'link': row['link'] or '',
            'label': row['label'] or '',
            'cover': row['cover'] or '',
            'date': row['fetch_date']
        })
    
    max_date = all_dates[0] if all_dates else datetime.now().strftime('%Y-%m-%d')
    min_date = all_dates[-1] if all_dates else max_date
    
    # 生成HTML
    html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>今日头条热榜 - 数据可视化</title>
    <style>
        * {{ margin: 0; padding: 0; box-sizing: border-box; }}
        body {{
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
            background: #f5f5f5;
            line-height: 1.6;
        }}
        
        .header {{
            background: linear-gradient(135deg, #ed4040 0%, #c62828 100%);
            color: white;
            padding: 25px 20px;
            position: sticky;
            top: 0;
            z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }}
        .header h1 {{ font-size: 24px; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; }}
        .header h1::before {{ content: "📰"; }}
        .header .meta {{ font-size: 13px; opacity: 0.9; }}
        
        .filters {{
            background: white;
            padding: 20px;
            border-bottom: 1px solid #e8e8e8;
            display: flex;
            gap: 15px;
            flex-wrap: wrap;
            align-items: flex-end;
        }}
        
        .filter-group {{ display: flex; flex-direction: column; gap: 6px; }}
        .filter-group label {{ font-size: 12px; color: #666; font-weight: 500; }}
        .filter-group select, .filter-group input {{
            padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;
            font-size: 14px; background: white; min-width: 120px;
        }}
        
        .btn {{ padding: 8px 16px; border: none; border-radius: 6px;
            cursor: pointer; font-size: 14px; background: #f0f0f0; color: #666; }}
        .btn:hover {{ background: #e0e0e0; }}
        
        .stats {{
            background: #fff0f0;
            padding: 12px 20px;
            font-size: 13px;
            color: #666;
            border-bottom: 1px solid #e8e8e8;
            display: flex;
            justify-content: space-between;
        }}
        .stats .highlight {{ color: #ed4040; font-weight: 600; }}
        
        .content {{ max-width: 900px; margin: 0 auto; padding: 20px; }}
        
        .hot-list {{ display: flex; flex-direction: column; gap: 12px; }}
        
        .hot-item {{
            display: flex;
            align-items: flex-start;
            padding: 16px 20px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.06);
            text-decoration: none;
            color: inherit;
            transition: transform 0.1s, box-shadow 0.1s;
        }}
        
        .hot-item:hover {{
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }}
        
        .rank {{
            width: 36px;
            height: 36px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: 700;
            font-size: 16px;
            margin-right: 16px;
            flex-shrink: 0;
        }}
        
        .rank.top1 {{ background: linear-gradient(135deg, #ff6b6b, #ee5a5a); color: white; }}
        .rank.top2 {{ background: linear-gradient(135deg, #ffa94d, #ff8c42); color: white; }}
        .rank.top3 {{ background: linear-gradient(135deg, #ffd43b, #fcc419); color: #333; }}
        .rank.normal {{ background: #f0f0f0; color: #666; }}
        
        .item-content {{ flex: 1; min-width: 0; }}
        
        .item-title {{
            font-size: 15px;
            color: #1a1a1a;
            margin-bottom: 8px;
            line-height: 1.5;
            word-break: break-word;
        }}
        
        .item-meta {{ display: flex; align-items: center; gap: 12px; font-size: 12px; }}
        
        .label-badge {{
            padding: 2px 10px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: 600;
            background: #ed4040;
            color: white;
        }}
        
        .heat-badge {{
            display: inline-flex;
            align-items: center;
            gap: 4px;
            color: #ed4040;
            font-weight: 500;
        }}
        
        .empty {{ text-align: center; padding: 80px 20px; color: #999; }}
        .empty-icon {{ font-size: 48px; margin-bottom: 16px; }}
        
        @media (max-width: 640px) {{
            .content {{ padding: 12px; }}
            .hot-item {{ padding: 12px 15px; }}
            .rank {{ width: 32px; height: 32px; font-size: 14px; margin-right: 12px; }}
        }}
    </style>
</head>
<body>
    <div class="header">
        <h1>今日头条热榜数据可视化</h1>
        <div class="meta">
            更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M')} | 
            共 {len(items)} 条记录
        </div>
    </div>
    
    <div class="filters">
        <div class="filter-group">
            <label>日期</label>
            <select id="dateSelect">
                <option value="">全部</option>
'''
    
    for date in all_dates[:30]:
        html += f'                <option value="{date}">{date}</option>\n'
    
    html += f'''            </select>
        </div>
        
        <div class="filter-group">
            <label>搜索</label>
            <input type="text" id="searchInput" placeholder="输入关键词...">
        </div>
        
        <button class="btn" onclick="resetFilters()">重置</button>
    </div>
    
    <div class="stats">
        <span>显示 <span class="highlight" id="showCount">{len(items)}</span> / {len(items)} 条</span>
    </div>
    
    <div class="content">
        <div class="hot-list" id="hotList"></div>
    </div>

    <script>
        const items = {json.dumps(items, ensure_ascii=False)};
        
        function formatHeat(num) {{
            if (num >= 10000) return (num / 10000).toFixed(1) + '万';
            return num.toLocaleString();
        }}
        
        function getRankClass(rank) {{
            if (rank === 1) return 'top1';
            if (rank === 2) return 'top2';
            if (rank === 3) return 'top3';
            return 'normal';
        }}
        
        function renderItems() {{
            const dateSelect = document.getElementById('dateSelect').value;
            const search = document.getElementById('searchInput').value.toLowerCase().trim();
            
            let filtered = items.filter(item => {{
                if (dateSelect && item.date !== dateSelect) return false;
                if (search && !item.title.toLowerCase().includes(search)) return false;
                return true;
            }});
            
            document.getElementById('showCount').textContent = filtered.length;
            
            const container = document.getElementById('hotList');
            if (filtered.length === 0) {{
                container.innerHTML = `
                    <div class="empty" style="grid-column: 1/-1;">
                        <div class="empty-icon">📰</div>
                        <div>没有找到记录</div>
                    </div>
                `;
                return;
            }}
            
            // 按日期分组
            const grouped = {{}};
            filtered.forEach(item => {{
                if (!grouped[item.date]) grouped[item.date] = [];
                grouped[item.date].push(item);
            }});
            
            let html = '';
            Object.keys(grouped).sort().reverse().forEach(date => {{
                html += grouped[date].map(item => `
                    <a href="${{item.link}}" class="hot-item" target="_blank">
                        <div class="rank ${{getRankClass(item.rank)}}">${{item.rank}}</div>
                        <div class="item-content">
                            <div class="item-title">${{item.title}}</div>
                            <div class="item-meta">
                                ${{item.label ? `<span class="label-badge">${{item.label}}</span>` : ''}}
                                ${{item.popularity ? `<span class="heat-badge">🔥 ${{formatHeat(item.popularity)}}</span>` : ''}}
                                <span style="color: #999;">${{item.date}}</span>
                            </div>
                        </div>
                    </a>
                `).join('');
            }});
            
            container.innerHTML = html;
        }}
        
        function resetFilters() {{
            document.getElementById('dateSelect').value = '';
            document.getElementById('searchInput').value = '';
            renderItems();
        }}
        
        document.getElementById('dateSelect').addEventListener('change', renderItems);
        document.getElementById('searchInput').addEventListener('input', renderItems);
        
        renderItems();
    </script>
</body>
</html>'''
    
    OUTPUT_PATH.write_text(html, encoding='utf-8')
    print(f"✅ HTML报告已生成: {OUTPUT_PATH}")
    print(f"   共 {len(items)} 条记录")
    print(f"   日期: {min_date} ~ {max_date}")

if __name__ == '__main__':
    generate_html()


path: scripts/init_db.py
content: #!/usr/bin/env python3
"""
Toutiao Hot - Database Initialization
今日头条热榜数据库初始化
"""

import sqlite3
from pathlib import Path

DB_PATH = Path(__file__).parent.parent / "data" / "toutiao.db"

def init_db():
    """初始化数据库"""
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    
    # 热榜条目表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS hot_items (
            id TEXT PRIMARY KEY,              -- 条目ID: clusterId+日期的哈希
            platform TEXT DEFAULT 'toutiao',  -- 平台标识
            rank INTEGER,                     -- 排名
            title TEXT NOT NULL,              -- 标题
            popularity INTEGER,               -- 热度值
            link TEXT,                        -- 链接
            cover TEXT,                       -- 封面图URL
            label TEXT,                       -- 标签
            cluster_id TEXT,                  -- 聚合ID
            categories TEXT,                  -- 兴趣分类(JSON)
            fetched_at INTEGER DEFAULT (strftime('%s', 'now')), -- 抓取时间
            fetch_date TEXT,                  -- 抓取日期(YYYY-MM-DD)
            raw_data TEXT                     -- 原始JSON
        )
    ''')
    
    # 创建索引
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_hot_items_date ON hot_items(fetch_date)')
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_hot_items_rank ON hot_items(rank)')
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_hot_items_label ON hot_items(label)')
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_hot_items_fetched ON hot_items(fetched_at)')
    
    # 抓取日志表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS fetch_logs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            started_at INTEGER DEFAULT (strftime('%s', 'now')),
            ended_at INTEGER,
            found INTEGER DEFAULT 0,
            new INTEGER DEFAULT 0,
            status INTEGER DEFAULT 0,
            error TEXT
        )
    ''')
    
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_fetch_logs_date ON fetch_logs(started_at)')
    
    conn.commit()
    conn.close()
    
    print(f"✅ Toutiao database initialized: {DB_PATH}")
    return DB_PATH

if __name__ == '__main__':
    init_db()


path: scripts/query.py
content: #!/usr/bin/env python3
"""
Toutiao Hot - Query Tool
今日头条热榜数据查询工具
"""

import sqlite3
from datetime import datetime, timezone
from pathlib import Path
import sys

DB_PATH = Path(__file__).parent.parent / "data" / "toutiao.db"

def query_today(limit=50):
    """查询今天的数据"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    
    today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
    
    cursor.execute('''
        SELECT rank, title, popularity, link, label
        FROM hot_items
        WHERE fetch_date = ?
        ORDER BY rank ASC
        LIMIT ?
    ''', (today, limit))
    
    rows = cursor.fetchall()
    conn.close()
    
    print(f"\n📰 今日头条热榜 - {today}")
    print("=" * 80)
    
    for row in rows:
        rank, title, popularity, link, label = row
        label_str = f" [{label}]" if label else ""
        pop_str = f"🔥 {popularity:,}" if popularity else ""
        print(f"  {rank:2d}. {title}{label_str}")
        if pop_str:
            print(f"      {pop_str}")
    
    print(f"\n   共 {len(rows)} 条数据")

def query_by_date(date_str, limit=50):
    """查询指定日期的数据"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    
    cursor.execute('''
        SELECT rank, title, popularity, link, label
        FROM hot_items
        WHERE fetch_date = ?
        ORDER BY rank ASC
        LIMIT ?
    ''', (date_str, limit))
    
    rows = cursor.fetchall()
    conn.close()
    
    if not rows:
        print(f"❌ 未找到 {date_str} 的数据")
        return
    
    print(f"\n📰 今日头条热榜 - {date_str}")
    print("=" * 80)
    
    for row in rows:
        rank, title, popularity, link, label = row
        label_str = f" [{label}]" if label else ""
        pop_str = f"🔥 {popularity:,}" if popularity else ""
        print(f"  {rank:2d}. {title}{label_str}")
        if pop_str:
            print(f"      {pop_str}")
    
    print(f"\n   共 {len(rows)} 条数据")

def query_stats(days=7):
    """查询统计"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    
    cursor.execute('''
        SELECT fetch_date, COUNT(*) as count, 
               SUM(popularity) as total_popularity
        FROM hot_items
        WHERE fetch_date >= date('now', '-{} days')
        GROUP BY fetch_date
        ORDER BY fetch_date DESC
    '''.format(days))
    
    rows = cursor.fetchall()
    conn.close()
    
    print(f"\n📊 最近{days}天统计")
    print("=" * 70)
    print(f"{'日期':<12} {'数量':<10} {'总热度':<15}")
    print("-" * 70)
    
    for row in rows:
        date_str, count, total_pop = row
        total_pop_str = f"{total_pop:,}" if total_pop else "0"
        print(f"{date_str:<12} {count:<10} {total_pop_str:<15}")

def query_logs(limit=10):
    """查询抓取日志"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    
    cursor.execute('''
        SELECT started_at, found, new, status
        FROM fetch_logs
        ORDER BY started_at DESC
        LIMIT ?
    ''', (limit,))
    
    rows = cursor.fetchall()
    conn.close()
    
    print(f"\n📝 最近{limit}次抓取日志")
    print("=" * 60)
    print(f"{'时间':<20} {'发现':<8} {'新增':<8} {'状态':<8}")
    print("-" * 60)
    
    for row in rows:
        started, found, new, status = row
        time_str = datetime.fromtimestamp(started, tz=timezone.utc).strftime('%m-%d %H:%M:%S')
        status_str = '✅' if status == 0 else '⚠️' if status == 1 else '❌'
        print(f"{time_str:<20} {found:<8} {new:<8} {status_str:<8}")

def main():
    args = sys.argv[1:]
    
    if not args or args[0] == 'today':
        limit = int(args[1]) if len(args) > 1 else 50
        query_today(limit)
    elif args[0] == 'date':
        if len(args) < 2:
            print("Usage: query.py date YYYY-MM-DD [limit]")
            return
        date_str = args[1]
        limit = int(args[2]) if len(args) > 2 else 50
        query_by_date(date_str, limit)
    elif args[0] == 'stats':
        days = int(args[1]) if len(args) > 1 else 7
        query_stats(days)
    elif args[0] == 'logs':
        limit = int(args[1]) if len(args) > 1 else 10
        query_logs(limit)
    else:
        print("Usage: query.py [command] [args]")
        print("")
        print("Commands:")
        print("  today [limit]           查询今天的数据")
        print("  date YYYY-MM-DD [limit] 查询指定日期的数据")
        print("  stats [days]            统计")
        print("  logs [limit]            抓取日志")

if __name__ == '__main__':
    main()


path: scripts/save_to_db.py
content: #!/usr/bin/env python3
"""
Toutiao Hot - Save to Database
将今日头条热榜保存到数据库
"""

import subprocess
import json
import sys
from pathlib import Path
from datetime import datetime, timezone

sys.path.insert(0, str(Path(__file__).parent))
from db import save_hot_items, log_fetch

def fetch_and_save(limit=50):
    """采集头条热榜并保存到数据库"""
    script_dir = Path(__file__).parent
    node_script = script_dir / "toutiao.js"
    
    print("🔥 采集今日头条热榜...")
    
    try:
        # 调用原有Node.js脚本获取数据
        result = subprocess.run(
            ['node', str(node_script), 'hot', str(limit)],
            capture_output=True,
            text=True,
            timeout=30
        )
        
        if result.returncode != 0:
            print(f"❌ 脚本执行失败: {result.stderr}")
            log_fetch(0, 0, status=2, error=result.stderr)
            return None
        
        # 解析JSON
        items = json.loads(result.stdout)
        print(f"   获取到 {len(items)} 条热榜")
        
        # 保存到数据库
        result = save_hot_items(items)
        
        # 记录日志
        status = 0 if result['errors'] == 0 else 1
        log_fetch(result['found'], result['new'], status=status)
        
        return result
        
    except subprocess.TimeoutExpired:
        print("❌ 采集超时")
        log_fetch(0, 0, status=2, error='Timeout')
        return None
    except Exception as e:
        print(f"❌ 异常: {e}")
        log_fetch(0, 0, status=2, error=str(e))
        return None

def main():
    """主函数"""
    args = sys.argv[1:]
    limit = int(args[0]) if args else 50
    
    print("=" * 60)
    print(f"📰 今日头条热榜采集 → 数据库")
    print(f"   数量: {limit}")
    print("=" * 60)
    
    # 确保数据库已初始化
    db_path = Path(__file__).parent.parent / "data" / "toutiao.db"
    if not db_path.exists():
        print("\n🔄 初始化数据库...")
        from init_db import init_db
        init_db()
    
    # 采集数据
    result = fetch_and_save(limit)
    
    if result:
        print("\n" + "=" * 60)
        print("✅ 完成!")
        print(f"   数据库: {db_path}")
        print(f"   发现: {result['found']} 条")
        print(f"   新增: {result['new']} 条")
        if result['errors'] > 0:
            print(f"   错误: {result['errors']} 条")
        
        # 显示Top 5
        print("\n🔥 Top 5:")
        from db import get_items_by_date
        items = get_items_by_date(limit=5)
        for item in items:
            label_str = f" [{item['label']}]" if item['label'] else ""
            pop_str = f"{item['popularity']:,}" if item['popularity'] else "N/A"
            print(f"  {item['rank']}. {item['title'][:40]}...{label_str}")
            print(f"     热度: {pop_str}")
    else:
        print("\n❌ 采集失败")
        sys.exit(1)

if __name__ == '__main__':
    main()


path: scripts/toutiao.js
content: #!/usr/bin/env node

/**
 * 今日头条热榜获取工具
 * 抓取 https://www.toutiao.com/hot-event/hot-board/ 返回的热点榜单数据
 */

const https = require('https');
const zlib = require('zlib');

const USER_AGENTS = [
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/123.0.0.0 Chrome/123.0.0.0 Safari/537.36',
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
  'Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0',
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0',
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0',
  'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
  'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36',
];

function getRandomUserAgent() {
  return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
}

const DEFAULT_HEADERS = {
  'Accept': 'application/json, text/plain, */*',
  'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  'Referer': 'https://www.toutiao.com/',
  'Origin': 'https://www.toutiao.com',
};

function decompressBody(buffer, contentEncoding) {
  if (!contentEncoding) return buffer;
  const encoding = String(contentEncoding).toLowerCase();
  if (encoding.includes('gzip')) return zlib.gunzipSync(buffer);
  if (encoding.includes('deflate')) return zlib.inflateSync(buffer);
  if (encoding.includes('br')) return zlib.brotliDecompressSync(buffer);
  return buffer;
}

/**
 * 发起 HTTP GET 请求并解析 JSON
 * @param {string} url
 * @param {object} headers
 */
function httpGetJson(url, headers = {}) {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname + urlObj.search,
      method: 'GET',
      headers: {
        ...DEFAULT_HEADERS,
        'User-Agent': getRandomUserAgent(),
        ...headers,
      },
    };

    const req = https.request(options, (res) => {
      const chunks = [];
      res.on('data', (chunk) => chunks.push(chunk));
      res.on('end', () => {
        try {
          const buffer = Buffer.concat(chunks);
          const decompressed = decompressBody(buffer, res.headers['content-encoding']);
          const text = decompressed.toString('utf-8');
          const data = JSON.parse(text);
          resolve(data);
        } catch (e) {
          const status = res.statusCode || 0;
          reject(new Error(`Failed to parse JSON (status=${status}): ${e.message}`));
        }
      });
    });

    req.on('error', reject);
    req.setTimeout(15000, () => {
      req.destroy();
      reject(new Error('Request timeout'));
    });
    req.end();
  });
}

async function getHotBoard(limit = 50) {
  const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(200, Math.floor(limit))) : 50;
  const url = 'https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc';
  const resp = await httpGetJson(url);

  if (!resp || !Array.isArray(resp.data)) {
    throw new Error('获取今日头条热榜失败：返回结构不符合预期');
  }

  const items = resp.data.map((item, index) => {
    let cleanedLink = '';
    try {
      const u = new URL(item.Url);
      u.search = '';
      u.hash = '';
      cleanedLink = u.toString();
    } catch {
      cleanedLink = typeof item.Url === 'string' ? item.Url : '';
    }

    const popularity = Number.parseInt(item.HotValue, 10);

    return {
      rank: index + 1,
      title: item.Title || '',
      popularity: Number.isFinite(popularity) ? popularity : 0,
      link: cleanedLink,
      cover: item.Image && item.Image.url ? item.Image.url : null,
      label: item.LabelDesc || item.Label || null,
      clusterId: String(item.ClusterIdStr || item.ClusterId || ''),
      categories: Array.isArray(item.InterestCategory) ? item.InterestCategory : [],
    };
  });

  return items.slice(0, safeLimit);
}

function printHelp() {
  console.log(`
今日头条热榜工具

用法:
  node scripts/toutiao.js <command> [args]

命令:
  hot, list            获取热榜（可选: limit）

示例:
  # 获取热榜（默认50条）
  node scripts/toutiao.js hot

  # 获取热榜前10条
  node scripts/toutiao.js hot 10
`);
}

async function main() {
  const args = process.argv.slice(2);
  const command = args[0];

  try {
    switch (command) {
      case 'hot':
      case 'list':
      case '--hot':
      case '-h': {
        const limitArg = args[1];
        const limit = limitArg ? Number.parseInt(limitArg, 10) : 50;
        const data = await getHotBoard(limit);
        console.log(JSON.stringify(data, null, 2));
        break;
      }
      case 'help':
      case '--help':
      case '-?':
      default:
        printHelp();
        process.exit(0);
    }
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
}

module.exports = { getHotBoard };

if (require.main === module) {
  main();
}


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