probe-douyin-search.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. #!/usr/bin/env node
  2. /**
  3. * 暴力探测 TikHub Douyin 搜索 endpoint 命名
  4. * 目标:找到一个能按关键词返回视频列表的 cookie-free 接口
  5. */
  6. const fs = require('fs');
  7. const path = require('path');
  8. const os = require('os');
  9. const https = require('https');
  10. const TOKEN = JSON.parse(fs.readFileSync(
  11. path.join(os.homedir(), '.openclaw', 'skills', 'xiaohongshu-search-notes', 'api-config.json'),
  12. 'utf-8'
  13. )).endpoint.headers.Authorization.replace(/^Bearer\s+/, '');
  14. function req(hostname, pathUrl, method = 'GET', body = null) {
  15. return new Promise((resolve) => {
  16. const headers = { Authorization: `Bearer ${TOKEN}`, Accept: 'application/json' };
  17. if (body) {
  18. headers['Content-Type'] = 'application/json';
  19. headers['Content-Length'] = Buffer.byteLength(body);
  20. }
  21. const r = https.request({ hostname, path: pathUrl, method, headers }, (res) => {
  22. const chunks = [];
  23. res.on('data', (c) => chunks.push(c));
  24. res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf-8') }));
  25. });
  26. r.on('error', (e) => resolve({ status: 0, body: e.message }));
  27. r.setTimeout(15000, () => { r.destroy(); resolve({ status: 0, body: 'timeout' }); });
  28. if (body) r.write(body);
  29. r.end();
  30. });
  31. }
  32. const kw = '猴菇饮';
  33. const kwE = encodeURIComponent(kw);
  34. // 根据 TikHub 抖音 endpoint 命名模式(app/v3 + web)
  35. const candidates = [
  36. // 直连 api.tikhub.io(官方域名)
  37. { host: 'api.tikhub.io', path: `/api/v1/douyin/app/v3/fetch_general_search_result?keyword=${kwE}&offset=0&count=20&sort_type=0&publish_time=0` },
  38. { host: 'api.tikhub.io', path: `/api/v1/douyin/web/fetch_general_search_result?keyword=${kwE}&offset=0&count=20` },
  39. { host: 'api.tikhub.io', path: `/api/v1/douyin/app/v3/fetch_video_search?keyword=${kwE}&offset=0&count=20` },
  40. { host: 'api.tikhub.io', path: `/api/v1/douyin/app/v3/fetch_user_search?keyword=${kwE}&offset=0&count=20` },
  41. { host: 'api.tikhub.io', path: `/api/v1/douyin/web/fetch_hot_search_list` },
  42. { host: 'api.tikhub.io', path: `/api/v1/douyin/app/v3/fetch_hot_search_list` },
  43. { host: 'api.tikhub.io', path: `/api/v1/douyin/web/fetch_word_hot_search_list?keyword=${kwE}` },
  44. // fmode 网关(已知工作)
  45. { host: 'server.fmode.cn', path: `/thapi/v1/douyin/app/v3/fetch_general_search_result?keyword=${kwE}&offset=0&count=20&sort_type=0&publish_time=0` },
  46. { host: 'server.fmode.cn', path: `/thapi/v1/douyin/web/fetch_general_search_result?keyword=${kwE}&offset=0&count=20` },
  47. { host: 'server.fmode.cn', path: `/thapi/v1/douyin/app/v3/fetch_video_search?keyword=${kwE}&offset=0&count=20` },
  48. { host: 'server.fmode.cn', path: `/thapi/v1/douyin/web/fetch_word_hot_search_list?keyword=${kwE}` },
  49. { host: 'server.fmode.cn', path: `/thapi/v1/douyin/web/fetch_hot_search_list` },
  50. // 关键词热度趋势
  51. { host: 'api.tikhub.io', path: `/api/v1/douyin/web/fetch_keyword_hot_trend?keyword=${kwE}` },
  52. { host: 'api.tikhub.io', path: `/api/v1/douyin/app/v3/fetch_keyword_hot_trend?keyword=${kwE}` },
  53. ];
  54. (async () => {
  55. console.log(`🔎 keyword = "${kw}"`);
  56. console.log(`🔑 token = ${TOKEN.slice(0, 10)}...\n`);
  57. for (const c of candidates) {
  58. const r = await req(c.host, c.path);
  59. let summary;
  60. try {
  61. const j = JSON.parse(r.body);
  62. const d = j.data ?? j;
  63. if (Array.isArray(d?.data)) summary = `data=Array(${d.data.length})`;
  64. else if (d?.aweme_list) summary = `aweme_list=${d.aweme_list.length}`;
  65. else if (d?.business_data) summary = `business_data=${Array.isArray(d.business_data) ? d.business_data.length : 'obj'}`;
  66. else if (d?.word_list) summary = `word_list=${d.word_list.length}`;
  67. else if (d?.hot_search_list) summary = `hot_search=${d.hot_search_list.length}`;
  68. else if (d?.detail) summary = `detail="${String(d.detail).slice(0, 80)}"`;
  69. else if (j.code) summary = `code=${j.code} msg=${(j.message || j.msg || '').slice(0, 60)}`;
  70. else summary = 'keys=' + Object.keys(d || j).slice(0, 5).join(',');
  71. } catch (e) {
  72. summary = r.body.slice(0, 100);
  73. }
  74. const flag = r.status === 200 ? '✅' : (r.status === 404 ? '✗' : '⚠');
  75. console.log(`${flag} [${r.status}] ${c.host}${c.path.slice(0, 75)}`);
  76. console.log(` → ${summary}\n`);
  77. await new Promise((r) => setTimeout(r, 500));
  78. }
  79. })();