tikhub.service.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { config } from '../config.ts';
  2. export interface ContentSample {
  3. noteId: string;
  4. title: string;
  5. content: string;
  6. publishTime: string;
  7. likeCount: number;
  8. commentCount: number;
  9. collectCount: number;
  10. shareCount: number;
  11. type: 'image' | 'video';
  12. }
  13. export interface UserProfile {
  14. userId: string;
  15. nickname: string;
  16. avatar: string;
  17. fansCount: number;
  18. notesCount: number;
  19. likeAndCollect: number;
  20. description: string;
  21. location: string;
  22. tags: string[];
  23. }
  24. /**
  25. * TikHub API 内容采样验证服务
  26. * 文档: https://docs.tikhub.io/4579297m0
  27. * 用于近10条内容采样、风格判断、活跃度校验
  28. */
  29. export async function searchNotes(keyword: string, platform: string = 'xiaohongshu', page: number = 1): Promise<ContentSample[]> {
  30. if (!config.tikHub.apiKey) {
  31. console.warn('[TikHub] No API key configured, returning empty results');
  32. return [];
  33. }
  34. try {
  35. const endpoint = getPlatformSearchEndpoint(platform);
  36. const response = await fetch(`${config.tikHub.baseUrl}${endpoint}`, {
  37. method: 'GET',
  38. headers: {
  39. Authorization: `Bearer ${config.tikHub.apiKey}`,
  40. 'Content-Type': 'application/json',
  41. },
  42. // @ts-ignore - URL search params
  43. ...buildSearchParams(keyword, page, platform),
  44. });
  45. if (!response.ok) {
  46. const errorText = await response.text();
  47. console.error(`[TikHub] Search error: ${response.status} - ${errorText}`);
  48. return [];
  49. }
  50. const data = await response.json();
  51. return normalizeContentSamples(data, platform);
  52. } catch (error) {
  53. console.error('[TikHub] Search failed:', error);
  54. return [];
  55. }
  56. }
  57. /**
  58. * 获取用户近期内容列表(用于内容风格判断)
  59. */
  60. export async function getUserNotes(userId: string, platform: string = 'xiaohongshu', count: number = 10): Promise<ContentSample[]> {
  61. if (!config.tikHub.apiKey) {
  62. return [];
  63. }
  64. try {
  65. const endpoint = getPlatformUserNotesEndpoint(platform);
  66. const queryParams = new URLSearchParams();
  67. queryParams.set('user_id', userId);
  68. queryParams.set('count', String(count));
  69. const response = await fetch(`${config.tikHub.baseUrl}${endpoint}?${queryParams.toString()}`, {
  70. method: 'GET',
  71. headers: {
  72. Authorization: `Bearer ${config.tikHub.apiKey}`,
  73. 'Content-Type': 'application/json',
  74. },
  75. });
  76. if (!response.ok) {
  77. return [];
  78. }
  79. const data = await response.json();
  80. return normalizeContentSamples(data, platform);
  81. } catch (error) {
  82. console.error('[TikHub] User notes fetch failed:', error);
  83. return [];
  84. }
  85. }
  86. /**
  87. * 获取用户个人资料
  88. */
  89. export async function getUserProfile(userId: string, platform: string = 'xiaohongshu'): Promise<UserProfile | null> {
  90. if (!config.tikHub.apiKey) {
  91. return null;
  92. }
  93. try {
  94. const endpoint = getPlatformUserProfileEndpoint(platform);
  95. const queryParams = new URLSearchParams();
  96. queryParams.set('user_id', userId);
  97. const response = await fetch(`${config.tikHub.baseUrl}${endpoint}?${queryParams.toString()}`, {
  98. method: 'GET',
  99. headers: {
  100. Authorization: `Bearer ${config.tikHub.apiKey}`,
  101. 'Content-Type': 'application/json',
  102. },
  103. });
  104. if (!response.ok) {
  105. return null;
  106. }
  107. const data = await response.json();
  108. return normalizeUserProfile(data, platform);
  109. } catch (error) {
  110. console.error('[TikHub] User profile fetch failed:', error);
  111. return null;
  112. }
  113. }
  114. function getPlatformSearchEndpoint(platform: string): string {
  115. switch (platform) {
  116. case 'xiaohongshu':
  117. return '/api/v1/xiaohongshu/web/search_notes';
  118. case 'douyin':
  119. return '/api/v1/douyin/web/search_videos';
  120. case 'bilibili':
  121. return '/api/v1/bilibili/web/search';
  122. default:
  123. return '/api/v1/xiaohongshu/web/search_notes';
  124. }
  125. }
  126. function getPlatformUserNotesEndpoint(platform: string): string {
  127. switch (platform) {
  128. case 'xiaohongshu':
  129. return '/api/v1/xiaohongshu/web/get_user_notes';
  130. case 'douyin':
  131. return '/api/v1/douyin/web/get_user_posts';
  132. case 'bilibili':
  133. return '/api/v1/bilibili/web/get_user_videos';
  134. default:
  135. return '/api/v1/xiaohongshu/web/get_user_notes';
  136. }
  137. }
  138. function getPlatformUserProfileEndpoint(platform: string): string {
  139. switch (platform) {
  140. case 'xiaohongshu':
  141. return '/api/v1/xiaohongshu/web/get_user_info';
  142. case 'douyin':
  143. return '/api/v1/douyin/web/get_user_info';
  144. case 'bilibili':
  145. return '/api/v1/bilibili/web/get_user_info';
  146. default:
  147. return '/api/v1/xiaohongshu/web/get_user_info';
  148. }
  149. }
  150. function buildSearchParams(keyword: string, page: number, platform: string): { body?: string } {
  151. // TikHub uses query params in URL, build accordingly
  152. const params = new URLSearchParams();
  153. params.set('keyword', keyword);
  154. params.set('page', String(page));
  155. if (platform === 'xiaohongshu') {
  156. params.set('sort', 'general');
  157. params.set('note_type', '0');
  158. }
  159. // Append to the URL instead of body
  160. return {};
  161. }
  162. function normalizeContentSamples(data: Record<string, unknown>, platform: string): ContentSample[] {
  163. const items = (data.data as Record<string, unknown>[] | undefined) || [];
  164. if (!Array.isArray(items)) return [];
  165. return items.map((item: Record<string, unknown>) => {
  166. if (platform === 'xiaohongshu') {
  167. return {
  168. noteId: String(item.note_id || item.id || ''),
  169. title: String(item.title || item.display_title || ''),
  170. content: String(item.desc || item.content || ''),
  171. publishTime: String(item.time || item.create_time || ''),
  172. likeCount: Number(item.liked_count || item.like_count || 0),
  173. commentCount: Number(item.comment_count || 0),
  174. collectCount: Number(item.collected_count || item.collect_count || 0),
  175. shareCount: Number(item.share_count || 0),
  176. type: (item.type === 'video' || item.note_type === 'video') ? 'video' : 'image',
  177. };
  178. }
  179. // Generic fallback for douyin/bilibili
  180. return {
  181. noteId: String(item.id || item.aweme_id || item.bvid || ''),
  182. title: String(item.title || item.desc || ''),
  183. content: String(item.content || item.desc || ''),
  184. publishTime: String(item.create_time || item.pubdate || ''),
  185. likeCount: Number(item.like_count || item.digg_count || item.like || 0),
  186. commentCount: Number(item.comment_count || item.reply || 0),
  187. collectCount: Number(item.collect_count || item.favorite || 0),
  188. shareCount: Number(item.share_count || 0),
  189. type: 'video',
  190. };
  191. });
  192. }
  193. function normalizeUserProfile(data: Record<string, unknown>, _platform: string): UserProfile | null {
  194. const user = (data.data as Record<string, unknown>) || data;
  195. if (!user) return null;
  196. return {
  197. userId: String(user.user_id || user.uid || ''),
  198. nickname: String(user.nickname || user.name || ''),
  199. avatar: String(user.avatar || user.avatar_url || ''),
  200. fansCount: Number(user.fans_count || user.follower_count || 0),
  201. notesCount: Number(user.notes_count || user.aweme_count || 0),
  202. likeAndCollect: Number(user.liked_and_collected || user.total_favorited || 0),
  203. description: String(user.description || user.desc || user.signature || ''),
  204. location: String(user.location || user.ip_location || ''),
  205. tags: Array.isArray(user.tags) ? user.tags as string[] : [],
  206. };
  207. }