const fs = require('fs'); const seedData = JSON.parse(fs.readFileSync('../hunt/seed_data.json', 'utf8')); // Sample 50 records evenly across platforms const platforms = ['bilibili', 'baidu', 'toutiao', 'zhihu', 'douban']; const byPlatform = {}; platforms.forEach(p => byPlatform[p] = seedData.filter(r => r.platform === p)); const selected = []; // Select: 20 bilibili, 12 baidu, 8 toutiao, 5 zhihu, 5 douban = 50 const quotas = { bilibili: 20, baidu: 12, toutiao: 8, zhihu: 5, douban: 5 }; for (const [p, q] of Object.entries(quotas)) { selected.push(...byPlatform[p].slice(0, q)); } function buildInput(record) { switch (record.platform) { case 'bilibili': return { platform: 'bilibili', endpoint: 'https://api.bilibili.com/x/web-interface/popular', raw_item: { title: record.topic, owner: { name: record.author || '未知UP主' }, stat: { view: record.hot_value, like: record.likes || 0, coin: 0, danmaku: 0 }, tnamev2: record.category, bvid: record.source_url.replace('https://b23.tv/', '') }, config: { required_fields: ['platform','rank','topic','category','source_url','timestamp'] } }; case 'baidu': return { platform: 'baidu', endpoint: 'https://top.baidu.com/board?tab=realtime', raw_html_fragment: `
${record.topic}
`, matched_query: record.topic, config: { required_fields: ['platform','rank','topic','category','source_url','timestamp'] } }; case 'toutiao': return { platform: 'toutiao', endpoint: 'https://www.toutiao.com/hot-event/hot-board/', raw_json_fragment: { Title: record.topic, Url: '/hot-event/hot-board/' }, config: { required_fields: ['platform','rank','topic','category','source_url','timestamp'] } }; case 'zhihu': return { platform: 'zhihu', endpoint: 'https://www.zhihu.com/api/v4/search/top_search', raw_item: { display_query: record.topic, query: record.topic }, config: { required_fields: ['platform','rank','topic','category','source_url','timestamp'] } }; case 'douban': return { platform: 'douban', endpoint: 'https://movie.douban.com/chart', raw_html_fragment: ``, matched_title: record.topic, config: { required_fields: ['platform','rank','topic','category','source_url','timestamp'] } }; default: return { platform: record.platform, raw: record.topic }; } } const samples = selected.map((record, idx) => ({ id: `tscn-${String(idx+1).padStart(3,'0')}`, input: buildInput(record), expected_output: { platform: record.platform, rank: record.rank, topic: record.topic, hot_value: record.hot_value, category: record.category, trend_direction: record.trend_direction, trend_change: record.trend_change || '0', source_url: record.source_url, timestamp: record.timestamp, crawl_time: record.crawl_time, author: record.author || null, likes: record.likes || null } })); // Add edge-case samples const edgeCases = [ { id: 'tscn-edge-01', input: { platform: 'bilibili', endpoint: 'api.bilibili.com/x/web-interface/popular', raw_item: { title: '', owner: {}, stat: {}, tnamev2: '', bvid: '' }, config: { required_fields: ['platform','rank','topic','category','source_url','timestamp'] } }, expected_output: { error: 'EMPTY_RECORD', message: '跳过缺失必填字段的记录', action: 'continue' }, note: '边界:B站返回空字段记录时的处理' }, { id: 'tscn-edge-02', input: { platform: 'baidu', endpoint: 'top.baidu.com/board?tab=realtime', raw_html_fragment: 'Forbidden', http_status: 403, config: { fallback: null } }, expected_output: { error: 'FORBIDDEN', message: '百度热榜返回 403,尝试增加延迟或切换 UA', action: 'retry_with_backoff', retry_delay_sec: 10 }, note: '边界:反爬触发 403 时的异常处理' }, { id: 'tscn-edge-03', input: { platform: 'weibo', endpoint: 'weibo.com/ajax/side/hotSearch', http_status: 418, config: { fallback_chain: ['weibo_hot_search_mobile', 'weibo_hot_search_html'], current_attempt: 1 } }, expected_output: { error: 'BLOCKED_418', message: '主端点 418,触发降级到移动端 API', action: 'trigger_fallback', next_source: 'weibo_hot_search_mobile' }, note: '边界:微博主 API 失败时的降级逻辑' }, { id: 'tscn-edge-04', input: { platform: 'zhihu', endpoint: 'zhihu.com/api/v3/feed/topstory/hot-lists/total', http_status: 401, config: { fallback: 'zhihu_hot_list_html' } }, expected_output: { error: 'AUTHENTICATION_ERROR', message: '知乎热榜需登录态,降级到 HTML 页面解析', action: 'trigger_fallback', next_source: 'zhihu_hot_list_html' }, note: '边界:需登录 API 失败时的降级' }, { id: 'tscn-edge-05', input: { platform: 'mixed', records: [ { platform: 'bilibili', topic: '重复话题A', timestamp: '2026-04-13T10:00:00Z' }, { platform: 'bilibili', topic: '重复话题A', timestamp: '2026-04-13T10:00:00Z' }, { platform: 'baidu', topic: '重复话题A', timestamp: '2026-04-13T10:00:00Z' } ], dedup_key: 'md5(platform + topic + timestamp)' }, expected_output: { kept: 2, removed: 1, reason: 'bilibili 记录因 (platform,topic,timestamp) 完全重复被去重', remaining_platforms: ['bilibili', 'baidu'] }, note: '边界:跨平台重复数据的去重逻辑' } ]; const allSamples = [...samples, ...edgeCases]; // Shuffle deterministically and split 80/20 allSamples.sort((a, b) => a.id.localeCompare(b.id)); const trainCount = Math.floor(allSamples.length * 0.8); const train = allSamples.slice(0, trainCount); const test = allSamples.slice(trainCount); fs.writeFileSync('train.jsonl', train.map(s => JSON.stringify(s)).join('\n') + '\n'); fs.writeFileSync('test.jsonl', test.map(s => JSON.stringify(s)).join('\n') + '\n'); console.log(`Total: ${allSamples.length} | Train: ${train.length} | Test: ${test.length}`);