This commit is contained in:
2025-05-01 03:59:37 +08:00
parent 26ca4c21c8
commit e58adbc46b
45 changed files with 7460 additions and 87 deletions

View File

@@ -0,0 +1,300 @@
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<string, any>;
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<string, string>;
private host: string;
private creatorHost: string;
private home: string;
private userAgent: string;
private cookies: Record<string, string> = {};
private headers: Record<string, string>;
constructor(
options: {
cookie?: string;
user_agent?: string;
timeout?: number;
sign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record<string, string>;
} = {},
) {
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<string, string>);
}
get cookieDict(): Record<string, string> {
return { ...this.cookies };
}
private async request(method: string, url: string, options: RequestInit = {}): Promise<any> {
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<string, any>, isCreator: boolean = false, options: RequestInit = {}): Promise<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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...
}