import asyncio
import json
import time
from collections.abc import Mapping
from urllib.parse import urlencode
import requests
from curl_cffi.requests import AsyncSession, Response
from typing import Dict
import os
import execjs
from numbers import Integral
from typing import Iterable, List, Optional, Tuple
import random
import base64
from xhs_auto_mcp.tools.log_utils import logger
class XhsApi:
def __init__(self,cookie):
self._cookie=cookie
self._base_url="https://edith.xiaohongshu.com"
self._headers = {
'content-type': 'application/json;charset=UTF-8',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
}
def init_session(self):
return AsyncSession(
verify=True,
impersonate="chrome124"
)
def _parse_cookie(self, cookie: str) -> Dict:
cookie_dict = {}
if cookie:
pairs = cookie.split(';')
for pair in pairs:
key, value = pair.strip().split('=', 1)
cookie_dict[key] = value
return cookie_dict
async def request(self,uri: str,session=None, method="GET",headers=None,params=None,data=None) -> Dict:
if session is None:
session=self.init_session()
if headers is None:
headers = {}
logger.debug(f"发送请求: {method} {self._base_url}{uri}")
if params:
logger.debug(f"请求参数: {params}")
if data:
logger.debug(f"请求数据: {data}")
response: Response = await session.request(
method=method,
url=f"{self._base_url}{uri}",
params=params,
json=data,
cookies=self._parse_cookie(self._cookie),
quote=False,
stream=True,
headers=headers
)
content = await response.acontent()
result = json.loads(content)
if 'success' in result:
if result['success']:
logger.debug(f"请求成功: {uri}")
else:
logger.warning(f"请求失败: {uri}, 错误信息: {result.get('msg', '未知错误')}")
return result
def base36encode(self,number: Integral, alphabet: Iterable[str] = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') -> str:
base36 = ''
alphabet = ''.join(alphabet)
sign = '-' if number < 0 else ''
number = abs(number)
while number:
number, i = divmod(number, len(alphabet))
base36 = alphabet[i] + base36
return sign + (base36 or alphabet[0])
def search_id(self):
e = int(time.time() * 1000) << 64
t = int(random.uniform(0, 2147483646))
return self.base36encode((e + t))
def get_xs_xt(self,uri, data, cookie):
current_directory = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(current_directory, "xhsvm.js")
try:
return execjs.compile(open(file_path, 'r', encoding='utf-8').read()).call('GetXsXt', uri, data, cookie)
except FileNotFoundError:
logger.error(f"找不到文件: {file_path}")
raise
async def get_me(self) -> Dict:
uri = '/api/sns/web/v2/user/me'
logger.info("获取用户信息")
return await self.request(uri,method="GET")
async def search_notes(self, keywords: str, limit: int = 20) -> Dict:
logger.info(f"搜索笔记: {keywords}, 限制: {limit}")
data={
"keyword":keywords,
"page":1,
"page_size":limit,
"search_id":self.search_id(),
"sort":"general",
"note_type":0,
"ext_flags":[],
"geo":"",
"image_formats":json.dumps(["jpg","webp","avif"], separators=(",", ":"))
}
return await self.request("/api/sns/web/v1/search/notes",method="POST",data=data)
async def home_feed(self) -> Dict:
data={
"category":"homefeed_recommend",
"cursor_score":"",
"image_formats":json.dumps(["jpg","webp","avif"], separators=(",", ":")),
"need_filter_image":False,
"need_num":8,
"num":18,
"note_index":33,
"refresh_type":1,
"search_key":"",
"unread_begin_note_id":"",
"unread_end_note_id":"",
"unread_note_count":0
}
uri="/api/sns/web/v1/homefeed"
p={"uri":uri,"method":"POST","data":data}
headers = {
'content-type': 'application/json;charset=UTF-8',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
}
xsxt=json.loads(self.get_xs_xt(uri,data,self._cookie))
headers['x-s']=xsxt['X-s']
headers['x-t']=str(xsxt['X-t'])
return await self.request(**p,headers=headers)
async def get_note_content(self, note_id: str, xsec_token: str) -> Dict:
data = {
"source_note_id": note_id,
"image_formats": ["jpg","webp","avif"],
"extra": {"need_body_topic":"1"},
"xsec_source": "pc_feed",
"xsec_token": xsec_token
}
uri="/api/sns/web/v1/feed"
p={"uri":uri,"method":"POST","data":data}
headers = {
'content-type': 'application/json;charset=UTF-8',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
}
xsxt=json.loads(self.get_xs_xt(uri,data,self._cookie))
headers['x-s']=xsxt['X-s']
headers['x-t']=str(xsxt['X-t'])
headers['x-s-common']='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'
return await self.request(**p,headers=headers)
async def get_note_comments(self,note_id:str,xsec_token:str) -> Dict:
uri = '/api/sns/web/v2/comment/page'
# 680a25a4000000001c02d251
# ABzm9YfVyNA1hsY-KwU7ybKNWlkpb8__t-jF9FwGKzZz0=
params = {
'note_id': note_id,
'cursor': '',
'top_comment_id': '',
'image_formats': 'jpg,webp,avif',
'xsec_token': xsec_token
}
return await self.request(uri,method="GET",params=params)
async def post_comment(self,note_id:str, comment: str) -> Dict:
uri='/api/sns/web/v1/comment/post'
# 680ce9d1000000001c02cb9f
data={
"note_id":note_id,
"content":comment,
"at_users":[]
}
headers = {
'content-type': 'application/json;charset=UTF-8',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
}
xsxt = json.loads(self.get_xs_xt(uri, data, self._cookie))
headers['x-s'] = xsxt['X-s']
headers['x-t'] = str(xsxt['X-t'])
return await self.request(uri, method="POST",headers=headers, data=data)