import { sign } from './helper.ts'; enum FeedType { RECOMMEND = 'homefeed_recommend', FASION = 'homefeed.fashion_v3', FOOD = 'homefeed.food_v3', COSMETICS = 'homefeed.cosmetics_v3', MOVIE = 'homefeed.movie_and_tv_v3', CAREER = 'homefeed.career_v3', EMOTION = 'homefeed.love_v3', HOURSE = 'homefeed.household_product_v3', GAME = 'homefeed.gaming_v3', TRAVEL = 'homefeed.travel_v3', FITNESS = 'homefeed.fitness_v3', } enum NoteType { NORMAL = 'normal', VIDEO = 'video', } enum SearchSortType { GENERAL = 'general', MOST_POPULAR = 'popularity_descending', LATEST = 'time_descending', } enum SearchNoteType { ALL = 0, VIDEO = 1, IMAGE = 2, } interface Note { note_id: string; title: string; desc: string; type: string; user: Record; img_urls: string[]; video_url: string; tag_list: any[]; at_user_list: any[]; collected_count: string; comment_count: string; liked_count: string; share_count: string; time: number; last_update_time: number; } interface ErrorResponse { success?: boolean; code?: number; msg?: string; data?: any; } class XhsError extends Error { response?: Response; verify_type?: string; verify_uuid?: string; constructor(message: string, options?: { response?: Response; verify_type?: string | null; verify_uuid?: string | null }) { super(message); this.response = options?.response; this.verify_type = options?.verify_type ?? ''; this.verify_uuid = options?.verify_uuid ?? ''; } } class DataFetchError extends XhsError {} class IPBlockError extends XhsError {} class NeedVerifyError extends XhsError {} class SignError extends XhsError {} export class XhsClientBase { private timeout: number; private externalSign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record; private host: string; private creatorHost: string; private home: string; private userAgent: string; private cookies: Record = {}; private headers: Record; constructor( options: { cookie?: string; user_agent?: string; timeout?: number; sign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record; } = {}, ) { this.timeout = options.timeout || 10 * 60 * 1000; // 10 minutes this.externalSign = options.sign; this.host = 'https://edith.xiaohongshu.com'; this.creatorHost = 'https://creator.xiaohongshu.com'; this.home = 'https://www.xiaohongshu.com'; this.userAgent = options.user_agent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'; this.headers = { 'user-agent': this.userAgent, 'Content-Type': 'application/json', }; if (options.cookie) { this.cookie = options.cookie; } } get cookie(): string { return Object.entries(this.cookies) .map(([key, value]) => `${key}=${value}`) .join('; '); } set cookie(cookie: string) { this.cookies = cookie.split(';').reduce((acc, pair) => { const [key, value] = pair.trim().split('='); if (key && value) { acc[key] = value; } return acc; }, {} as Record); } get cookieDict(): Record { return { ...this.cookies }; } private async request(method: string, url: string, options: RequestInit = {}): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { method, headers: { ...this.headers, ...options.headers, cookie: this.cookie, }, body: options.body, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } if (response.status === 204) { // No content return response; } const data = await response.json().catch(() => response.text()); if (response.status === 471 || response.status === 461) { const verify_type = response.headers.get('Verifytype'); const verify_uuid = response.headers.get('Verifyuuid'); throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verify_type},Verifyuuid: ${verify_uuid}`, { response, verify_type, verify_uuid }); } if (data.success) { return data.data || data.success; } else if (data.code === 300007) { // IP_BLOCK throw new IPBlockError('IP blocked', { response }); } else if (data.code === 400002) { // SIGN_FAULT throw new SignError('Sign fault', { response }); } else { throw new DataFetchError(data, { response }); } } catch (error) { clearTimeout(timeoutId); if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error(`Request timeout after ${this.timeout} seconds`); } throw error; } throw new Error('Unknown error occurred'); } } preHeaders(url: string, data?: any, isCreator: boolean = false): void { if (isCreator) { // Implement sign function in TypeScript or import it const signs = sign(url, data, { a1: this.cookies.a1 }); this.headers['x-s'] = signs['x-s']; this.headers['x-t'] = signs['x-t']; this.headers['x-s-common'] = signs['x-s-common']; } else if (this.externalSign) { const signs = this.externalSign(url, data, { a1: this.cookies.a1, web_session: this.cookies.web_session || '', }); Object.assign(this.headers, signs); } } async get(uri: string, params?: Record, isCreator: boolean = false, options: RequestInit = {}): Promise { let finalUri = uri; if (params) { const query = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined) { query.append(key, String(value)); } } finalUri = `${uri}?${query.toString()}`; } this.preHeaders(finalUri, undefined, isCreator); const url = `${isCreator ? this.creatorHost : this.host}${finalUri}`; return this.request('GET', url, options); } async post(uri: string, data: any, isCreator: boolean = false, options: RequestInit = {}): Promise { this.preHeaders(uri, data, isCreator); const url = `${isCreator ? this.creatorHost : this.host}${uri}`; return this.request('POST', url, { ...options, body: JSON.stringify(data), }); } async getNoteById(noteId: string): Promise { const data = { source_note_id: noteId, image_scenes: ['CRD_WM_WEBP'] }; const uri = '/api/sns/web/v1/feed'; const res = await this.post(uri, data); return res.items[0].note_card; } async getNoteByIdFromHtml(noteId: string): Promise { const url = `${this.home}/explore/${noteId}`; const response = await this.request('GET', url, { headers: { 'user-agent': this.userAgent, referer: `${this.home}/`, }, }); if (typeof response === 'string') { const html = response; const stateMatch = html.match(/window\.__INITIAL_STATE__=({.*?})<\/script>/); if (!stateMatch) { throw new Error('Could not find initial state in HTML'); } let state = stateMatch[1].replace(/undefined/g, '""'); if (state === '{}') { throw new DataFetchError('Empty state'); } // Implement transformJsonKeys in TypeScript if needed const noteDict = this.transformJsonKeys(JSON.parse(state)); return noteDict.note.note_detail_map[noteId].note; } throw new DataFetchError('Invalid response'); } private transformJsonKeys(data: any): any { // Implement camelToUnderscore and transform logic here // Similar to the Python version return data; // Placeholder } // Implement all other methods similarly, converting Python to TypeScript // For example: async reportNoteMetrics( noteId: string, noteType: number, noteUserId: string, viewerUserId: string, followedAuthor: number = 0, reportType: number = 1, staySeconds: number = 0, ): Promise { const uri = '/api/sns/web/v1/note/metrics_report'; const data = { note_id: noteId, note_type: noteType, report_type: reportType, stress_test: false, viewer: { user_id: viewerUserId, followed_author: followedAuthor }, author: { user_id: noteUserId }, interaction: { like: 0, collect: 0, comment: 0, comment_read: 0 }, note: { stay_seconds: staySeconds }, other: { platform: 'web' }, }; return this.post(uri, data); } // Continue with all other methods... }