#!/usr/bin/env node /** * TikHub Ultimate Probe * 一口气验证三类接口(15+ 端点): * A. Douyin Billboard(热榜系列,POST) * B. TikTok Ads Insights(创意中心,GET) * C. TikTok Shop Web / App API(GET) * 把每个端点的 status + 关键字段 dump 到 data/probe-ultimate-.json */ const fs = require('fs'); const path = require('path'); const os = require('os'); const https = require('https'); const TIKHUB_TOKEN = JSON.parse( fs.readFileSync( path.join(os.homedir(), '.openclaw', 'skills', 'xiaohongshu-search-notes', 'api-config.json'), 'utf-8' ) ).endpoint.headers.Authorization.replace(/^Bearer\s+/, ''); const OUT_DIR = path.join('data', 'probe-ultimate'); if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true }); function request(method, apiPath, { params = {}, body = null } = {}) { const qs = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null && v !== '') .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); const fullPath = qs ? `${apiPath}?${qs}` : apiPath; const bodyStr = body ? JSON.stringify(body) : null; return new Promise((resolve) => { const headers = { Authorization: `Bearer ${TIKHUB_TOKEN}`, Accept: 'application/json' }; if (bodyStr) { headers['Content-Type'] = 'application/json'; headers['Content-Length'] = Buffer.byteLength(bodyStr); } const opts = { hostname: 'api.tikhub.io', path: fullPath, method, headers }; const req = https.request(opts, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const raw = Buffer.concat(chunks).toString('utf-8'); try { resolve({ status: res.statusCode, json: JSON.parse(raw), rawLen: raw.length }); } catch (e) { resolve({ status: res.statusCode, json: { _raw: raw.slice(0, 300) }, rawLen: raw.length }); } }); }); req.on('error', (e) => resolve({ status: 0, json: { _error: e.message } })); req.setTimeout(30000, () => { req.destroy(); resolve({ status: 0, json: { _error: 'timeout' } }); }); if (bodyStr) req.write(bodyStr); req.end(); }); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // ============================================================ // 探针列表 // ============================================================ const PROBES = [ // ---- A. Douyin Billboard(POST)---- { group: 'A-douyin-billboard', name: 'fetch_hot_total_video_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_video_list', body: { date_window: 24, keyword: '益生菌', page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_topic_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_topic_list', body: { date_window: 24, keyword: '益生菌', page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_search_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_search_list', body: { date_window: 24, keyword: '益生菌', page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_hot_word_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_hot_word_list', body: { date_window: 24, page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_high_fan_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_high_fan_list', body: { date_window: 24, page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_high_like_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_high_like_list', body: { date_window: 24, page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_high_play_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_high_play_list', body: { date_window: 24, page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_high_topic_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_high_topic_list', body: { date_window: 24, page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_total_high_search_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_total_high_search_list', body: { date_window: 24, page: 1, page_size: 10 } }, { group: 'A-douyin-billboard', name: 'fetch_hot_account_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_account_list', body: { date_window: 24, page: 1, page_size: 10, tags: '1' } }, { group: 'A-douyin-billboard', name: 'fetch_hot_calendar_list', method: 'POST', path: '/api/v1/douyin/billboard/fetch_hot_calendar_list', body: { start_date: 20260101, end_date: 20260417, city_code: '', category_code: '' } }, // Douyin Web hot search(不需要 body) { group: 'A-douyin-billboard', name: 'web_fetch_hot_search_result', method: 'GET', path: '/api/v1/douyin/web/fetch_hot_search_result' }, // ---- B. TikTok Ads Creative Center(GET)---- { group: 'B-tiktok-ads', name: 'get_keyword_list', method: 'GET', path: '/api/v1/tiktok/ads/get_keyword_list', params: { period: 30, country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'get_keyword_details', method: 'GET', path: '/api/v1/tiktok/ads/get_keyword_details', params: { keyword: 'milk thistle', period: 30, country_code: 'US' } }, { group: 'B-tiktok-ads', name: 'get_keyword_insights', method: 'GET', path: '/api/v1/tiktok/ads/get_keyword_insights', params: { keyword: 'milk thistle', period: 30, country_code: 'US' } }, { group: 'B-tiktok-ads', name: 'get_related_keywords', method: 'GET', path: '/api/v1/tiktok/ads/get_related_keywords', params: { keyword: 'milk thistle', period: 30, country_code: 'US' } }, { group: 'B-tiktok-ads', name: 'get_top_products', method: 'GET', path: '/api/v1/tiktok/ads/get_top_products', params: { period: 30, country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'get_popular_hashtags', method: 'GET', path: '/api/v1/tiktok/ads/get_popular_hashtags', params: { period: 30, country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'get_popular_sound', method: 'GET', path: '/api/v1/tiktok/ads/get_popular_sound', params: { period: 30, country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'get_top_ads_spotlight', method: 'GET', path: '/api/v1/tiktok/ads/get_top_ads_spotlight', params: { period: 30, country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'get_creator_list', method: 'GET', path: '/api/v1/tiktok/ads/get_creator_list', params: { country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'search_creators', method: 'GET', path: '/api/v1/tiktok/ads/search_creators', params: { keyword: 'probiotic', country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'get_trend_popular_videos', method: 'GET', path: '/api/v1/tiktok/ads/get_trend_popular_videos', params: { period: 30, country_code: 'US', page: 1, limit: 10 } }, { group: 'B-tiktok-ads', name: 'creator_search_insights', method: 'GET', path: '/api/v1/tiktok/app/v3/creator_search_insights', params: { keyword: 'milk thistle' } }, { group: 'B-tiktok-ads', name: 'creator_search_insights_trend', method: 'GET', path: '/api/v1/tiktok/app/v3/creator_search_insights_trend', params: { keyword: 'milk thistle' } }, // ---- C. TikTok Shop Web(GET)---- { group: 'C-tiktok-shop', name: 'shop_search_products_v1', method: 'GET', path: '/api/v1/tiktok/shop/fetch_search_products_v1', params: { keyword: 'milk thistle', country_code: 'US', page: 1, size: 10 } }, { group: 'C-tiktok-shop', name: 'shop_search_products_v3', method: 'GET', path: '/api/v1/tiktok/shop/fetch_search_products_v3', params: { keyword: 'milk thistle', country_code: 'US', page: 1, size: 10 } }, { group: 'C-tiktok-shop', name: 'shop_search_keyword_suggest_v1', method: 'GET', path: '/api/v1/tiktok/shop/fetch_search_keyword_suggest_v1', params: { keyword: 'milk thistle', country_code: 'US' } }, { group: 'C-tiktok-shop', name: 'shop_get_hot_selling_products', method: 'GET', path: '/api/v1/tiktok/shop/fetch_hot_selling_products', params: { country_code: 'US', page: 1, size: 10 } }, { group: 'C-tiktok-shop', name: 'shop_get_product_category_list', method: 'GET', path: '/api/v1/tiktok/shop/fetch_product_category_list', params: { country_code: 'US' } }, { group: 'C-tiktok-shop', name: 'shop_get_product_reviews_v2', method: 'GET', path: '/api/v1/tiktok/shop/fetch_product_reviews_v2', params: { product_id: '1730213111571944226', country_code: 'US', page: 1, size: 10 } }, // 占位 ID,用来测路径 ]; function summarizeJson(j, maxLen = 300) { if (!j) return { _empty: true }; const topKeys = Object.keys(j).slice(0, 8); const dataPreview = (() => { if (j.data) { if (Array.isArray(j.data)) return { _arrayLen: j.data.length, _first: j.data[0] ? Object.keys(j.data[0]).slice(0, 6) : null }; if (typeof j.data === 'object') return { _keys: Object.keys(j.data).slice(0, 8) }; } return null; })(); return { topKeys, message: j.message || j.msg, code: j.code, error: j.error, dataPreview }; } async function main() { console.log('==== TikHub Ultimate Probe ===='); const results = []; let okCount = 0, failCount = 0; for (const p of PROBES) { process.stdout.write(`[${p.group}] ${p.method} ${p.name} `); const resp = await request(p.method, p.path, { params: p.params, body: p.body }); const ok = resp.status === 200 && !(resp.json && (resp.json.code && resp.json.code !== 0 && resp.json.code !== 200)); if (ok) okCount++; else failCount++; const summary = summarizeJson(resp.json); const rec = { group: p.group, name: p.name, method: p.method, path: p.path, params: p.params || null, body: p.body || null, status: resp.status, ok, rawLen: resp.rawLen, summary, sampleJson: resp.status === 200 ? resp.json : { errorJson: resp.json }, }; fs.writeFileSync(path.join(OUT_DIR, `${p.name}.json`), JSON.stringify(rec, null, 2), 'utf-8'); console.log(`→ ${resp.status}${ok ? ' ✓' : ' ✗'} ${summary.message ? '| ' + String(summary.message).slice(0, 60) : ''}`); results.push(rec); await sleep(600); } const overview = results.map((r) => ({ group: r.group, name: r.name, method: r.method, path: r.path, status: r.status, ok: r.ok, msg: (r.summary?.message || '').slice(0, 80), dataPreview: r.summary?.dataPreview, })); fs.writeFileSync(path.join(OUT_DIR, '_overview.json'), JSON.stringify(overview, null, 2), 'utf-8'); console.log('\n==== 汇总 ===='); console.log(`OK: ${okCount} / Fail: ${failCount} / Total: ${PROBES.length}`); console.log('\nByGroup:'); ['A-douyin-billboard', 'B-tiktok-ads', 'C-tiktok-shop'].forEach((g) => { const gs = overview.filter((x) => x.group === g); const gOk = gs.filter((x) => x.ok).length; console.log(` ${g}: ${gOk}/${gs.length} OK`); gs.forEach((x) => console.log(` [${x.ok ? '✓' : '✗'}] ${x.name} ${x.status} ${x.msg || ''}`)); }); console.log(`\nDetails → ${OUT_DIR}/_overview.json`); } main().catch((e) => { console.error(e); process.exit(1); });