probe-tiktok-endpoints2.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env node
  2. /**
  3. * TikTok 第二轮探测:视频详情 / 评论 / 用户画像 / 用户作品
  4. * 先用 fetch_general_search_result 搜 "milk thistle" 拿一个 aweme_id + sec_uid,
  5. * 然后用这俩去测深度端点
  6. */
  7. const fs = require('fs');
  8. const path = require('path');
  9. const os = require('os');
  10. const https = require('https');
  11. const TOKEN = JSON.parse(
  12. fs.readFileSync(
  13. path.join(os.homedir(), '.openclaw', 'skills', 'xiaohongshu-search-notes', 'api-config.json'),
  14. 'utf-8'
  15. )
  16. ).endpoint.headers.Authorization.replace(/^Bearer\s+/, '');
  17. function get(apiPath) {
  18. return new Promise((resolve) => {
  19. const opts = {
  20. hostname: 'api.tikhub.io',
  21. path: apiPath,
  22. method: 'GET',
  23. headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'application/json' },
  24. };
  25. const req = https.request(opts, (res) => {
  26. const chunks = [];
  27. res.on('data', (c) => chunks.push(c));
  28. res.on('end', () => {
  29. const body = Buffer.concat(chunks).toString('utf-8');
  30. try { resolve({ status: res.statusCode, json: JSON.parse(body) }); }
  31. catch (e) { resolve({ status: res.statusCode, text: body.slice(0, 300) }); }
  32. });
  33. });
  34. req.on('error', (e) => resolve({ status: 0, error: e.message }));
  35. req.setTimeout(45000, () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
  36. req.end();
  37. });
  38. }
  39. const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  40. (async () => {
  41. // 第一步:拿一个视频样本
  42. console.log('Step 1: Search "milk thistle" to get sample aweme_id + sec_uid');
  43. const s = await get('/api/v1/tiktok/app/v3/fetch_general_search_result?keyword=milk%20thistle&cursor=0&count=10');
  44. fs.writeFileSync('data/tiktok-probe-search-raw.json', JSON.stringify(s.json, null, 2).slice(0, 12000), 'utf-8');
  45. const d = s.json?.data;
  46. console.log(' data type:', typeof d, 'isArray:', Array.isArray(d));
  47. if (d && typeof d === 'object') {
  48. console.log(' data keys:', Array.isArray(d) ? `arr[${d.length}]` : Object.keys(d).slice(0, 8).join(','));
  49. }
  50. // 统一找一个 aweme_info:可能在 arr 顶层,可能在 data.data,可能在 data.aweme_list
  51. let videoList = [];
  52. if (Array.isArray(d)) videoList = d;
  53. else if (Array.isArray(d?.data)) videoList = d.data;
  54. else if (Array.isArray(d?.aweme_list)) videoList = d.aweme_list;
  55. else if (Array.isArray(d?.item_list)) videoList = d.item_list;
  56. console.log(' videoList length:', videoList.length);
  57. const firstVideo = videoList.find((x) => x?.aweme_info) || videoList[0];
  58. const videoInfo = firstVideo?.aweme_info || firstVideo;
  59. const aweme_id = videoInfo?.aweme_id;
  60. const sec_uid = videoInfo?.author?.sec_uid;
  61. const author_unique_id = videoInfo?.author?.unique_id;
  62. console.log(' aweme_id:', aweme_id, '| sec_uid:', sec_uid?.slice(0, 20) + '...', '| unique:', author_unique_id);
  63. console.log(' video desc:', (videoInfo?.desc || '').slice(0, 80));
  64. console.log(' video stats:', JSON.stringify(videoInfo?.statistics || {}).slice(0, 150));
  65. if (videoInfo) fs.writeFileSync('data/tiktok-probe-sample-video.json', JSON.stringify(videoInfo, null, 2).slice(0, 8000), 'utf-8');
  66. if (!aweme_id) {
  67. console.log('❌ no aweme_id found, aborting depth probe. See data/tiktok-probe-search-raw.json for raw');
  68. return;
  69. }
  70. // 第二步:试深度端点
  71. const depthPaths = [
  72. // 视频详情
  73. `/api/v1/tiktok/app/v3/fetch_one_video?aweme_id=${aweme_id}`,
  74. `/api/v1/tiktok/app/v3/fetch_video_detail?aweme_id=${aweme_id}`,
  75. // 视频评论
  76. `/api/v1/tiktok/app/v3/fetch_video_comments?aweme_id=${aweme_id}&cursor=0&count=10`,
  77. // 作者资料
  78. `/api/v1/tiktok/app/v3/handler_user_profile?sec_user_id=${sec_uid}`,
  79. `/api/v1/tiktok/app/v3/fetch_user_profile?sec_user_id=${sec_uid}`,
  80. `/api/v1/tiktok/web/fetch_user_profile?unique_id=${author_unique_id}`,
  81. // 作者作品
  82. `/api/v1/tiktok/app/v3/fetch_user_post_videos?sec_user_id=${sec_uid}&max_cursor=0&count=10`,
  83. `/api/v1/tiktok/app/v3/fetch_user_post?sec_user_id=${sec_uid}&cursor=0&count=10`,
  84. ];
  85. const results = [];
  86. for (const p of depthPaths) {
  87. await sleep(700);
  88. const r = await get(p);
  89. const code = r.json?.code;
  90. const msg = (r.json?.message_zh || r.json?.message || '').slice(0, 60);
  91. const d = r.json?.data;
  92. let hint = '';
  93. if (d) {
  94. if (Array.isArray(d)) hint = `arr[${d.length}]`;
  95. else if (typeof d === 'object') {
  96. hint = 'keys=' + Object.keys(d).slice(0, 7).join(',');
  97. const inner = d.aweme_detail || d.comments || d.aweme_list || d.user || d.user_info;
  98. if (inner) {
  99. if (Array.isArray(inner)) hint += ` inner=arr[${inner.length}]`;
  100. else hint += ' inner_keys=' + Object.keys(inner).slice(0, 5).join(',');
  101. }
  102. }
  103. }
  104. console.log(`[${r.status}] ${p.split('?')[0].replace('/api/v1/tiktok/', '')}`);
  105. console.log(` code=${code} msg="${msg}" ${hint}`);
  106. results.push({ path: p, status: r.status, code, msg, hint });
  107. }
  108. fs.writeFileSync('data/tiktok-probe2.json', JSON.stringify(results, null, 2), 'utf-8');
  109. console.log('\n✓ data/tiktok-probe2.json');
  110. })();