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: `
`,
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}`);