| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- 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<ContentSample[]> {
- 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<ContentSample[]> {
- 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<UserProfile | null> {
- 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<DouyinUserSearchResult[]> {
- 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<string, unknown>);
- } 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<string, unknown>, platform: string): ContentSample[] {
- const items = (data.data as Record<string, unknown>[] | undefined) || [];
- if (!Array.isArray(items)) return [];
- return items.map((item: Record<string, unknown>) => {
- 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<string, unknown>, _platform: string): UserProfile | null {
- const user = (data.data as Record<string, unknown>) || 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<string, unknown>): DouyinUserSearchResult[] {
- const root = data.data as Record<string, unknown> | 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<string, unknown>).dynamic_patch as Record<string, unknown> | undefined)?.raw_data;
- if (typeof rawData !== 'string') continue;
- try {
- const parsed = JSON.parse(rawData) as Record<string, unknown>;
- const userInfo = parsed.user_info as Record<string, unknown> | 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<string, unknown> | 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] : '';
- }
|