import { config } from '../config.ts'; export interface ContentSample { noteId: string; title: string; content: string; publishTime: string; likeCount: number; commentCount: number; collectCount: number; shareCount: number; type: 'image' | 'video'; } export interface UserProfile { userId: string; nickname: string; avatar: string; fansCount: number; notesCount: number; likeAndCollect: number; description: string; location: string; tags: string[]; } export interface DouyinUserSearchResult { uid: string; secUid: string; uniqueId: string; nickname: string; followerCount: number; avatar: string; } /** * TikHub API 内容采样验证服务 * 文档: https://docs.tikhub.io/4579297m0 * 用于近10条内容采样、风格判断、活跃度校验 */ export async function searchNotes(keyword: string, platform: string = 'xiaohongshu', page: number = 1): Promise { if (!config.tikHub.apiKey) { console.warn('[TikHub] No API key configured, returning empty results'); return []; } try { const endpoint = getPlatformSearchEndpoint(platform); const response = await fetch(`${config.tikHub.baseUrl}${endpoint}`, { method: 'GET', headers: { Authorization: `Bearer ${config.tikHub.apiKey}`, 'Content-Type': 'application/json', }, // @ts-ignore - URL search params ...buildSearchParams(keyword, page, platform), }); if (!response.ok) { const errorText = await response.text(); console.error(`[TikHub] Search error: ${response.status} - ${errorText}`); return []; } const data = await response.json(); return normalizeContentSamples(data, platform); } catch (error) { console.error('[TikHub] Search failed:', error); return []; } } /** * 获取用户近期内容列表(用于内容风格判断) */ export async function getUserNotes(userId: string, platform: string = 'xiaohongshu', count: number = 10): Promise { if (!config.tikHub.apiKey) { return []; } try { const endpoint = getPlatformUserNotesEndpoint(platform); const queryParams = new URLSearchParams(); queryParams.set('user_id', userId); queryParams.set('count', String(count)); const response = await fetch(`${config.tikHub.baseUrl}${endpoint}?${queryParams.toString()}`, { method: 'GET', headers: { Authorization: `Bearer ${config.tikHub.apiKey}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { return []; } const data = await response.json(); return normalizeContentSamples(data, platform); } catch (error) { console.error('[TikHub] User notes fetch failed:', error); return []; } } /** * 获取用户个人资料 */ export async function getUserProfile(userId: string, platform: string = 'xiaohongshu'): Promise { if (!config.tikHub.apiKey) { return null; } try { const endpoint = getPlatformUserProfileEndpoint(platform); const queryParams = new URLSearchParams(); queryParams.set('user_id', userId); const response = await fetch(`${config.tikHub.baseUrl}${endpoint}?${queryParams.toString()}`, { method: 'GET', headers: { Authorization: `Bearer ${config.tikHub.apiKey}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { return null; } const data = await response.json(); return normalizeUserProfile(data, platform); } catch (error) { console.error('[TikHub] User profile fetch failed:', error); return null; } } export async function searchDouyinUsers(keyword: string): Promise { if (!config.tikHub.apiKey || !keyword.trim()) return []; try { const response = await fetch(`${config.tikHub.baseUrl}/api/v1/douyin/search/fetch_user_search`, { method: 'POST', headers: { Authorization: `Bearer ${config.tikHub.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ keyword, cursor: 0 }), }); if (!response.ok) { const errorText = await response.text(); console.error(`[TikHub] Douyin user search error: ${response.status} - ${errorText}`); return []; } const data = await response.json(); return normalizeDouyinUserSearchResults(data as Record); } catch (error) { console.error('[TikHub] Douyin user search failed:', error); return []; } } function getPlatformSearchEndpoint(platform: string): string { switch (platform) { case 'xiaohongshu': return '/api/v1/xiaohongshu/web/search_notes'; case 'douyin': return '/api/v1/douyin/web/search_videos'; case 'bilibili': return '/api/v1/bilibili/web/search'; default: return '/api/v1/xiaohongshu/web/search_notes'; } } function getPlatformUserNotesEndpoint(platform: string): string { switch (platform) { case 'xiaohongshu': return '/api/v1/xiaohongshu/web/get_user_notes'; case 'douyin': return '/api/v1/douyin/web/get_user_posts'; case 'bilibili': return '/api/v1/bilibili/web/get_user_videos'; default: return '/api/v1/xiaohongshu/web/get_user_notes'; } } function getPlatformUserProfileEndpoint(platform: string): string { switch (platform) { case 'xiaohongshu': return '/api/v1/xiaohongshu/web/get_user_info'; case 'douyin': return '/api/v1/douyin/web/get_user_info'; case 'bilibili': return '/api/v1/bilibili/web/get_user_info'; default: return '/api/v1/xiaohongshu/web/get_user_info'; } } function buildSearchParams(keyword: string, page: number, platform: string): { body?: string } { // TikHub uses query params in URL, build accordingly const params = new URLSearchParams(); params.set('keyword', keyword); params.set('page', String(page)); if (platform === 'xiaohongshu') { params.set('sort', 'general'); params.set('note_type', '0'); } // Append to the URL instead of body return {}; } function normalizeContentSamples(data: Record, platform: string): ContentSample[] { const items = (data.data as Record[] | undefined) || []; if (!Array.isArray(items)) return []; return items.map((item: Record) => { if (platform === 'xiaohongshu') { return { noteId: String(item.note_id || item.id || ''), title: String(item.title || item.display_title || ''), content: String(item.desc || item.content || ''), publishTime: String(item.time || item.create_time || ''), likeCount: Number(item.liked_count || item.like_count || 0), commentCount: Number(item.comment_count || 0), collectCount: Number(item.collected_count || item.collect_count || 0), shareCount: Number(item.share_count || 0), type: (item.type === 'video' || item.note_type === 'video') ? 'video' : 'image', }; } // Generic fallback for douyin/bilibili return { noteId: String(item.id || item.aweme_id || item.bvid || ''), title: String(item.title || item.desc || ''), content: String(item.content || item.desc || ''), publishTime: String(item.create_time || item.pubdate || ''), likeCount: Number(item.like_count || item.digg_count || item.like || 0), commentCount: Number(item.comment_count || item.reply || 0), collectCount: Number(item.collect_count || item.favorite || 0), shareCount: Number(item.share_count || 0), type: 'video', }; }); } function normalizeUserProfile(data: Record, _platform: string): UserProfile | null { const user = (data.data as Record) || data; if (!user) return null; return { userId: String(user.user_id || user.uid || ''), nickname: String(user.nickname || user.name || ''), avatar: String(user.avatar || user.avatar_url || ''), fansCount: Number(user.fans_count || user.follower_count || 0), notesCount: Number(user.notes_count || user.aweme_count || 0), likeAndCollect: Number(user.liked_and_collected || user.total_favorited || 0), description: String(user.description || user.desc || user.signature || ''), location: String(user.location || user.ip_location || ''), tags: Array.isArray(user.tags) ? user.tags as string[] : [], }; } function normalizeDouyinUserSearchResults(data: Record): DouyinUserSearchResult[] { const root = data.data as Record | undefined; const userList = root?.user_list; if (!Array.isArray(userList)) return []; const results: DouyinUserSearchResult[] = []; for (const item of userList) { if (typeof item !== 'object' || item === null) continue; const rawData = ((item as Record).dynamic_patch as Record | undefined)?.raw_data; if (typeof rawData !== 'string') continue; try { const parsed = JSON.parse(rawData) as Record; const userInfo = parsed.user_info as Record | undefined; if (!userInfo) continue; results.push({ uid: String(userInfo.uid || ''), secUid: String(userInfo.sec_uid || ''), uniqueId: String(userInfo.unique_id || ''), nickname: String(userInfo.nickname || ''), followerCount: Number(userInfo.follower_count || 0), avatar: firstUrl((userInfo.avatar_larger as Record | undefined)?.url_list), }); } catch { // Ignore malformed raw_data entries from search results. } } return results.filter((user) => user.uid && user.secUid && user.nickname); } function firstUrl(value: unknown): string { return Array.isArray(value) && typeof value[0] === 'string' ? value[0] : ''; }