n8n-nodes-xiaohongshu
Version:
n8n node for Xiaohongshu (Little Red Book) API integration
660 lines (659 loc) • 25.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.API_OPERATIONS = exports.API_CATEGORIES = void 0;
exports.getSignParams = getSignParams;
exports.xiaohongshuApiRequest = xiaohongshuApiRequest;
exports.getApiCategories = getApiCategories;
exports.getApisByCategory = getApisByCategory;
exports.getOperationDetails = getOperationDetails;
exports.parseCookieString = parseCookieString;
exports.getCookieValue = getCookieValue;
exports.buildQueryString = buildQueryString;
exports.parseUserUrl = parseUserUrl;
exports.parseNoteUrl = parseNoteUrl;
exports.buildDynamicRequestParams = buildDynamicRequestParams;
const n8n_workflow_1 = require("n8n-workflow");
/**
* 小红书 API 分类定义
*/
exports.API_CATEGORIES = [
{
name: '搜索相关',
value: 'search',
description: '搜索笔记、用户等功能',
},
{
name: '笔记相关',
value: 'note',
description: '获取笔记信息、评论等',
},
{
name: '用户相关',
value: 'user',
description: '获取用户信息、用户笔记等',
},
];
/**
* 小红书 API 接口定义
*/
exports.API_OPERATIONS = {
search: [
{
name: '搜索笔记',
value: 'searchNotes',
description: '根据关键词搜索笔记',
method: 'POST',
url: '/api/sns/web/v1/search/notes',
defaultBody: {
keyword: '',
page: 1,
page_size: 20,
search_id: '',
sort: 'general',
note_type: 0,
ext_flags: [],
image_formats: ['jpg', 'webp', 'avif']
}
},
{
name: '搜索用户',
value: 'searchUsers',
description: '根据关键词搜索用户',
method: 'POST',
url: '/api/sns/web/v1/search/usersearch',
defaultBody: {
search_user_request: {
keyword: '',
page: 1,
page_size: 20,
biz_type: 'web_search_user',
search_id: '',
request_id: ''
}
}
},
{
name: '获取搜索建议',
value: 'getSearchSuggestion',
description: '获取搜索关键词建议',
method: 'GET',
url: '/api/sns/web/v1/search/recommend',
defaultParams: {
keyword: ''
}
},
],
note: [
{
name: '获取笔记详情',
value: 'getNoteInfo',
description: '根据笔记ID获取详细信息',
method: 'POST',
url: '/api/sns/web/v1/feed',
defaultBody: {
source_note_id: '',
image_formats: ['jpg', 'webp', 'avif'],
xsec_token: '',
xsec_source: 'pc_search',
extra: {
need_body_topic: '1'
}
}
},
{
name: '获取笔记评论',
value: 'getNoteComments',
description: '获取笔记的评论列表',
method: 'GET',
url: '/api/sns/web/v2/comment/page',
defaultParams: {
note_id: '',
cursor: '',
top_comment_id: '',
image_formats: 'jpg,webp,avif',
xsec_token: ''
}
},
{
name: '获取子评论',
value: 'getNoteSubComments',
description: '获取评论的回复列表',
method: 'GET',
url: '/api/sns/web/v2/comment/sub/page',
defaultParams: {
note_id: '',
root_comment_id: '',
num: 30,
cursor: ''
}
},
{
name: '获取用户笔记',
value: 'getUserNotes',
description: '获取用户发布的笔记列表',
method: 'GET',
url: '/api/sns/web/v1/user_posted',
defaultParams: {
num: 30,
cursor: '',
user_id: '',
image_scenes: 'FD_WM_WEBP',
xsec_token: '',
xsec_source: 'pc_feed'
}
},
],
user: [
{
name: '获取用户信息',
value: 'getUserInfo',
description: '根据用户链接获取用户信息',
method: 'GET',
url: '/api/sns/web/v1/user/otherinfo',
defaultParams: {
target_user_id: '',
xsec_token: ''
}
},
{
name: '获取个人信息',
value: 'getSelfInfo',
description: '获取当前登录用户的个人信息',
method: 'GET',
url: '/api/sns/web/v1/user/selfinfo'
},
],
};
/**
* 获取签名参数
*/
async function getSignParams(uri, data, cookies) {
var _a;
const cookieCredentials = await this.getCredentials('xiaohongshuCookieApi');
// 构建请求头
const headers = {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
};
// 只有当Token存在、不为空且长度合理时才添加Authorization头
const token = (_a = cookieCredentials.signServiceToken) === null || _a === void 0 ? void 0 : _a.toString().trim();
if (token && token.length > 10) { // Token通常比较长,少于10个字符可能是无效的
headers['Authorization'] = `Bearer ${token}`;
console.log('使用Token认证,Token长度:', token.length);
}
else {
console.log('不使用Token认证 - Token为空或无效');
}
const signRequestOptions = {
method: 'POST',
url: cookieCredentials.signServiceUrl,
headers,
body: {
uri,
data: data || {},
cookies,
},
json: true,
timeout: 30000, // 30秒超时
};
try {
// 添加调试日志
console.log('发送签名请求:', JSON.stringify(signRequestOptions, null, 2));
const response = await this.helpers.httpRequest(signRequestOptions);
// 添加成功响应日志
console.log('签名服务响应:', JSON.stringify(response, null, 2));
return response;
}
catch (error) {
// 添加更详细的错误信息
let errorMessage = `获取签名参数失败: ${error instanceof Error ? error.message : String(error)}`;
if (error.response) {
errorMessage += `\n状态码: ${error.response.status}`;
errorMessage += `\n响应体: ${JSON.stringify(error.response.data || error.response.body)}`;
}
if (error.request) {
errorMessage += `\n请求URL: ${signRequestOptions.url}`;
errorMessage += `\n请求体: ${JSON.stringify(signRequestOptions.body)}`;
}
// 尝试简化data参数重试
if (signRequestOptions.body && typeof signRequestOptions.body === 'object' &&
signRequestOptions.body.data &&
Object.keys(signRequestOptions.body.data).length > 0) {
console.log('尝试用空data重试...');
const retryOptions = {
...signRequestOptions,
body: {
...signRequestOptions.body,
data: {}
}
};
try {
const retryResponse = await this.helpers.httpRequest(retryOptions);
console.log('重试成功:', JSON.stringify(retryResponse, null, 2));
return retryResponse;
}
catch (retryError) {
console.log('重试也失败了');
}
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage);
}
}
/**
* 发送小红书 API 请求
*/
async function xiaohongshuApiRequest(options) {
const cookieCredentials = await this.getCredentials('xiaohongshuCookieApi');
// 对于GET请求,需要将参数拼接到URI中用于签名计算
let uriForSignature = options.url;
let finalUrl = options.url;
if (options.method === 'GET' && options.params) {
// 构建查询字符串
const queryString = buildQueryString(options.params);
if (queryString) {
uriForSignature = `${options.url}?${queryString}`;
finalUrl = uriForSignature;
}
}
// 获取签名参数
const signParams = await getSignParams.call(this, uriForSignature, options.data || {}, cookieCredentials.cookie);
// 构建完整的请求选项
const requestOptions = {
method: options.method,
url: `https://edith.xiaohongshu.com${finalUrl}`,
headers: {
'accept': 'application/json, text/plain, */*',
'cache-control': 'no-cache',
'content-type': 'application/json;charset=UTF-8',
'cookie': cookieCredentials.cookie,
'origin': 'https://www.xiaohongshu.com',
'referer': 'https://www.xiaohongshu.com/',
'user-agent': cookieCredentials.userAgent,
'x-s': signParams['x-s'],
'x-t': signParams['x-t'],
'x-s-common': signParams['x-s-common'],
...options.headers,
},
json: true,
};
// 添加请求体
if (options.data) {
requestOptions.body = options.data;
}
try {
const response = await this.helpers.httpRequest(requestOptions);
return response;
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `小红书 API 请求失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* 获取 API 分类
*/
async function getApiCategories() {
return exports.API_CATEGORIES;
}
/**
* 根据分类获取 API 接口
*/
async function getApisByCategory(category) {
return exports.API_OPERATIONS[category] || [];
}
/**
* 获取特定操作的详细信息
*/
async function getOperationDetails(category, operation) {
const operations = exports.API_OPERATIONS[category] || [];
return operations.find((op) => op.value === operation);
}
/**
* 解析 Cookie 字符串
*/
function parseCookieString(cookieString) {
const cookies = {};
cookieString.split(';').forEach((cookie) => {
const [name, value] = cookie.trim().split('=');
if (name && value) {
cookies[name] = value;
}
});
return cookies;
}
/**
* 获取 Cookie 中的特定值
*/
function getCookieValue(cookieString, name) {
const cookies = parseCookieString(cookieString);
return cookies[name] || null;
}
/**
* 构建查询字符串
*/
function buildQueryString(params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== null && value !== undefined && value !== '') {
searchParams.append(key, value.toString());
}
}
return searchParams.toString();
}
/**
* 解析小红书用户链接URL
*/
function parseUserUrl(url) {
try {
const urlObj = new URL(url);
// 解析用户ID - 从路径中提取
const pathMatch = urlObj.pathname.match(/\/user\/profile\/([a-f0-9]+)/);
const userId = pathMatch ? pathMatch[1] : undefined;
// 解析查询参数
const xsecToken = urlObj.searchParams.get('xsec_token') || undefined;
const xsecSource = urlObj.searchParams.get('xsec_source') || undefined;
return {
userId,
xsecToken,
xsecSource,
};
}
catch (error) {
console.error('解析用户URL失败:', error);
return {};
}
}
/**
* 解析小红书笔记链接URL
*/
function parseNoteUrl(url) {
try {
const urlObj = new URL(url);
// 解析笔记ID - 从路径中提取
const pathMatch = urlObj.pathname.match(/\/explore\/([a-f0-9]+)/);
const noteId = pathMatch ? pathMatch[1] : undefined;
// 解析查询参数
const xsecToken = urlObj.searchParams.get('xsec_token') || undefined;
const xsecSource = urlObj.searchParams.get('xsec_source') || undefined;
return {
noteId,
xsecToken,
xsecSource,
};
}
catch (error) {
console.error('解析笔记URL失败:', error);
return {};
}
}
/**
* 构建动态请求参数
*/
function buildDynamicRequestParams(executeFunctions, category, operation, itemIndex) {
const operations = exports.API_OPERATIONS[category] || [];
const operationDetail = operations.find((op) => op.value === operation);
if (!operationDetail) {
return {};
}
const requestParams = {
method: operationDetail.method,
url: operationDetail.url,
};
// 处理 GET 请求的查询参数
if (operationDetail.method === 'GET' && operationDetail.defaultParams) {
requestParams.params = {};
// 先检查是否需要解析链接
let parsedUrlData = {};
console.log('当前操作:', operation, '分类:', category);
// 检查用户链接
if (['getUserInfo', 'getUserNotes'].includes(operation)) {
try {
const userUrl = executeFunctions.getNodeParameter('user_url', itemIndex);
if (userUrl && userUrl.trim()) {
parsedUrlData = parseUserUrl(userUrl);
console.log('解析用户链接结果:', parsedUrlData);
}
else {
console.log('用户链接为空,操作:', operation);
}
}
catch (error) {
// 用户链接参数不存在或为空
console.log('用户链接参数获取失败:', error);
}
}
// 检查笔记链接
if (['getNoteComments', 'getNoteSubComments'].includes(operation)) {
try {
const noteUrl = executeFunctions.getNodeParameter('note_url', itemIndex);
if (noteUrl && noteUrl.trim()) {
parsedUrlData = parseNoteUrl(noteUrl);
console.log('解析笔记链接结果:', parsedUrlData);
console.log('笔记链接:', noteUrl);
}
else {
console.log('笔记链接为空,操作:', operation);
}
}
catch (error) {
// 笔记链接参数不存在或为空
console.log('笔记链接参数获取失败:', error);
}
}
Object.keys(operationDetail.defaultParams).forEach((paramName) => {
try {
let paramValue = executeFunctions.getNodeParameter(paramName, itemIndex);
// 优先使用解析出来的参数值(无论用户是否输入了值)
if (parsedUrlData) {
if (paramName === 'user_id' && parsedUrlData.userId) {
paramValue = parsedUrlData.userId;
console.log('使用解析的用户ID:', paramValue);
}
else if (paramName === 'target_user_id' && parsedUrlData.userId) {
paramValue = parsedUrlData.userId;
console.log('使用解析的目标用户ID:', paramValue);
}
else if (paramName === 'note_id' && parsedUrlData.noteId) {
paramValue = parsedUrlData.noteId;
console.log('使用解析的笔记ID:', paramValue);
}
else if (paramName === 'xsec_token' && parsedUrlData.xsecToken) {
paramValue = parsedUrlData.xsecToken;
console.log('使用解析的xsec_token:', paramValue);
}
else if (paramName === 'xsec_source' && parsedUrlData.xsecSource) {
paramValue = parsedUrlData.xsecSource;
console.log('使用解析的xsec_source:', paramValue);
}
}
// 总是添加参数,即使为空值
if (paramValue !== undefined && paramValue !== null && paramValue !== '') {
requestParams.params[paramName] = paramValue;
console.log(`添加参数 ${paramName}:`, paramValue);
}
else {
// 即使参数为空,也要添加到请求中(某些API需要空值参数)
const defaultValue = operationDetail.defaultParams[paramName];
requestParams.params[paramName] = defaultValue;
console.log(`添加默认参数 ${paramName}:`, defaultValue);
}
}
catch (error) {
// 参数不存在,检查是否能从解析的URL数据中获取
let paramValue = operationDetail.defaultParams[paramName];
// 优先使用解析出来的参数值
if (parsedUrlData) {
if (paramName === 'user_id' && parsedUrlData.userId) {
paramValue = parsedUrlData.userId;
console.log('参数获取失败,使用解析的用户ID:', paramValue);
}
else if (paramName === 'target_user_id' && parsedUrlData.userId) {
paramValue = parsedUrlData.userId;
console.log('参数获取失败,使用解析的目标用户ID:', paramValue);
}
else if (paramName === 'note_id' && parsedUrlData.noteId) {
paramValue = parsedUrlData.noteId;
console.log('参数获取失败,使用解析的笔记ID:', paramValue);
}
else if (paramName === 'xsec_token' && parsedUrlData.xsecToken) {
paramValue = parsedUrlData.xsecToken;
console.log('参数获取失败,使用解析的xsec_token:', paramValue);
}
else if (paramName === 'xsec_source' && parsedUrlData.xsecSource) {
paramValue = parsedUrlData.xsecSource;
console.log('参数获取失败,使用解析的xsec_source:', paramValue);
}
}
requestParams.params[paramName] = paramValue;
if (paramValue !== operationDetail.defaultParams[paramName]) {
console.log(`参数获取失败,使用解析的值 ${paramName}:`, paramValue);
}
else {
console.log(`参数获取失败,使用默认值 ${paramName}:`, paramValue);
}
}
});
// 调试信息:输出最终的GET请求参数
console.log('最终GET请求参数:', requestParams.params);
}
// 处理 POST 请求的请求体
if (['POST', 'PUT'].includes(operationDetail.method) && operationDetail.defaultBody) {
requestParams.data = { ...operationDetail.defaultBody };
// 检查是否需要解析笔记URL
let noteUrlData = {};
if (['getNoteInfo'].includes(operation)) {
try {
const noteUrl = executeFunctions.getNodeParameter('note_url', itemIndex);
if (noteUrl && noteUrl.trim()) {
noteUrlData = parseNoteUrl(noteUrl);
console.log('解析笔记链接结果:', noteUrlData);
console.log('笔记链接:', noteUrl);
}
else {
console.log('笔记链接为空,操作:', operation);
}
}
catch (error) {
// 笔记链接参数不存在或为空
}
}
// 递归处理请求体中的参数
requestParams.data = processBodyParams(requestParams.data, executeFunctions, itemIndex, noteUrlData);
// 特殊处理需要动态生成的参数
requestParams.data = processSpecialParams(requestParams.data, operation);
// 调试信息:输出最终的POST请求体
console.log('最终POST请求体:', requestParams.data);
}
return requestParams;
}
/**
* 递归处理请求体参数
*/
function processBodyParams(obj, executeFunctions, itemIndex, noteUrlData) {
if (Array.isArray(obj)) {
return obj;
}
if (obj && typeof obj === 'object') {
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
// 递归处理嵌套对象
result[key] = processBodyParams(value, executeFunctions, itemIndex, noteUrlData);
}
else {
// 尝试从节点参数获取值
try {
let paramValue = executeFunctions.getNodeParameter(key, itemIndex);
// 优先使用解析出来的参数值
if (noteUrlData) {
if (key === 'source_note_id' && noteUrlData.noteId) {
paramValue = noteUrlData.noteId;
}
else if (key === 'note_id' && noteUrlData.noteId) {
paramValue = noteUrlData.noteId;
}
else if (key === 'note_oid' && noteUrlData.noteId) {
paramValue = noteUrlData.noteId;
}
else if (key === 'xsec_token' && noteUrlData.xsecToken) {
paramValue = noteUrlData.xsecToken;
}
else if (key === 'xsec_source' && noteUrlData.xsecSource) {
paramValue = noteUrlData.xsecSource;
}
}
if (paramValue !== undefined && paramValue !== null && paramValue !== '') {
result[key] = paramValue;
}
else {
result[key] = value;
}
}
catch (error) {
// 参数不存在,检查是否能从解析的URL数据中获取
let paramValue = value;
if (noteUrlData) {
if (key === 'source_note_id' && noteUrlData.noteId) {
paramValue = noteUrlData.noteId;
}
else if (key === 'note_id' && noteUrlData.noteId) {
paramValue = noteUrlData.noteId;
}
else if (key === 'note_oid' && noteUrlData.noteId) {
paramValue = noteUrlData.noteId;
}
else if (key === 'xsec_token' && noteUrlData.xsecToken) {
paramValue = noteUrlData.xsecToken;
}
else if (key === 'xsec_source' && noteUrlData.xsecSource) {
paramValue = noteUrlData.xsecSource;
}
}
result[key] = paramValue;
}
}
}
return result;
}
return obj;
}
/**
* 处理特殊参数(需要动态生成的参数)
*/
function processSpecialParams(data, operation) {
if (!data || typeof data !== 'object') {
return data;
}
// 深拷贝避免修改原始数据
const result = JSON.parse(JSON.stringify(data));
// 处理不同操作的特殊参数
switch (operation) {
case 'searchNotes':
// 生成搜索ID
if (result.search_id === '') {
result.search_id = `search_${Date.now()}`;
}
break;
case 'searchUsers':
// 处理搜索用户的嵌套结构
if (result.search_user_request) {
if (result.search_user_request.search_id === '') {
result.search_user_request.search_id = `user_search_${Date.now()}`;
}
if (result.search_user_request.request_id === '') {
result.search_user_request.request_id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
break;
case 'getNoteInfo':
// 确保xsec_source设置正确
if (!result.xsec_source) {
result.xsec_source = 'pc_search';
}
// 确保extra对象存在
if (!result.extra) {
result.extra = { need_body_topic: '1' };
}
break;
}
return result;
}