UNPKG

n8n-nodes-xiaohongshu

Version:

n8n node for Xiaohongshu (Little Red Book) API integration

660 lines (659 loc) 25.3 kB
"use strict"; 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; }