mcp-xiaohongshu
Version:
A friendly tool to help you access and interact with Xiaohongshu (RedNote) content through Model Context Protocol.
898 lines (897 loc) • 44.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedNoteTools = void 0;
const authManager_1 = require("../auth/authManager");
const logger_1 = __importDefault(require("../utils/logger"));
const noteDetail_1 = require("./noteDetail");
class RedNoteTools {
constructor() {
this.browser = null;
this.page = null;
logger_1.default.info('Initializing RedNoteTools');
this.authManager = new authManager_1.AuthManager();
}
async initialize() {
logger_1.default.info('Initializing browser and page');
this.browser = await this.authManager.getBrowser();
if (!this.browser) {
throw new Error('Failed to initialize browser');
}
try {
this.page = await this.browser.newPage();
// Load cookies if available
const cookies = await this.authManager.getCookies();
if (cookies.length > 0) {
logger_1.default.info(`Loading ${cookies.length} cookies`);
await this.page.context().addCookies(cookies);
}
// Check login status
logger_1.default.info('Checking login status');
await this.page.goto('https://www.xiaohongshu.com');
const isLoggedIn = await this.page.evaluate(() => {
const sidebarUser = document.querySelector('.user.side-bar-component .channel');
return sidebarUser?.textContent?.trim() === '我';
});
// If not logged in, perform login
if (!isLoggedIn) {
logger_1.default.error('Not logged in, please login first');
throw new Error('Not logged in');
}
logger_1.default.info('Login status verified');
}
catch (error) {
// 初始化过程中出错,确保清理资源
await this.cleanup();
throw error;
}
}
async cleanup() {
logger_1.default.info('Cleaning up browser resources');
try {
if (this.page) {
await this.page.close().catch(err => logger_1.default.error('Error closing page:', err));
this.page = null;
}
if (this.browser) {
await this.browser.close().catch(err => logger_1.default.error('Error closing browser:', err));
this.browser = null;
}
}
catch (error) {
logger_1.default.error('Error during cleanup:', error);
}
finally {
this.page = null;
this.browser = null;
}
}
extractRedBookUrl(shareText) {
// 匹配 http://xhslink.com/ 开头的链接
const xhslinkRegex = /(https?:\/\/xhslink\.com\/[a-zA-Z0-9\/]+)/i;
const xhslinkMatch = shareText.match(xhslinkRegex);
if (xhslinkMatch && xhslinkMatch[1]) {
return xhslinkMatch[1];
}
// 匹配 https://www.xiaohongshu.com/ 开头的链接
const xiaohongshuRegex = /(https?:\/\/(?:www\.)?xiaohongshu\.com\/[^,\s]+)/i;
const xiaohongshuMatch = shareText.match(xiaohongshuRegex);
if (xiaohongshuMatch && xiaohongshuMatch[1]) {
return xiaohongshuMatch[1];
}
return shareText;
}
async searchNotes(keywords, limit = 10) {
logger_1.default.info(`Searching notes with keywords: ${keywords}, limit: ${limit}`);
try {
await this.initialize();
if (!this.page)
throw new Error('Page not initialized');
// Navigate to search page
logger_1.default.info('Navigating to search page');
await this.page.goto(`https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(keywords)}`);
// Wait for search results to load
logger_1.default.info('Waiting for search results');
await this.page.waitForSelector('.feeds-container', {
timeout: 30000
});
// Get all note items
let noteItems = await this.page.$$('.feeds-container .note-item');
logger_1.default.info(`Found ${noteItems.length} note items`);
const notes = [];
// Process each note
for (let i = 0; i < Math.min(noteItems.length, limit); i++) {
logger_1.default.info(`Processing note ${i + 1}/${Math.min(noteItems.length, limit)}`);
try {
// Click on the note cover to open detail
await noteItems[i].$eval('a.cover.mask.ld', (el) => el.click());
// Wait for the note page to load
logger_1.default.info('Waiting for note page to load');
await this.page.waitForSelector('#noteContainer', {
timeout: 30000
});
await this.randomDelay(0.5, 1.5);
// Extract note content
const note = await this.page.evaluate(() => {
const article = document.querySelector('#noteContainer');
if (!article)
return null;
// Get title
const titleElement = article.querySelector('#detail-title');
const title = titleElement?.textContent?.trim() || '';
// Get content
const contentElement = article.querySelector('#detail-desc .note-text');
const content = contentElement?.textContent?.trim() || '';
// Get author info
const authorElement = article.querySelector('.author-wrapper .username');
const author = authorElement?.textContent?.trim() || '';
// Get interaction counts from engage-bar
const engageBar = document.querySelector('.engage-bar-style');
const likesElement = engageBar?.querySelector('.like-wrapper .count');
const likes = parseInt(likesElement?.textContent?.replace(/[^\d]/g, '') || '0');
const collectElement = engageBar?.querySelector('.collect-wrapper .count');
const collects = parseInt(collectElement?.textContent?.replace(/[^\d]/g, '') || '0');
const commentsElement = engageBar?.querySelector('.chat-wrapper .count');
const comments = parseInt(commentsElement?.textContent?.replace(/[^\d]/g, '') || '0');
return {
title,
content,
url: window.location.href,
author,
likes,
collects,
comments
};
});
if (note) {
logger_1.default.info(`Extracted note: ${note.title}`);
notes.push(note);
}
// Add random delay before closing
await this.randomDelay(0.5, 1);
// Close note by clicking the close button
const closeButton = await this.page.$('.close-circle');
if (closeButton) {
logger_1.default.info('Closing note dialog');
await closeButton.click();
// Wait for note dialog to disappear
await this.page.waitForSelector('#noteContainer', {
state: 'detached',
timeout: 30000
});
}
}
catch (error) {
logger_1.default.error(`Error processing note ${i + 1}:`, error);
const closeButton = await this.page.$('.close-circle');
if (closeButton) {
logger_1.default.info('Attempting to close note dialog after error');
await closeButton.click();
// Wait for note dialog to disappear
await this.page.waitForSelector('#noteContainer', {
state: 'detached',
timeout: 30000
});
}
}
finally {
// Add random delay before next note
await this.randomDelay(0.5, 1.5);
}
}
logger_1.default.info(`Successfully processed ${notes.length} notes`);
return notes;
}
catch (error) {
logger_1.default.error('Error searching notes:', error);
throw error;
}
finally {
await this.cleanup();
}
}
async getNoteContent(url) {
logger_1.default.info(`Getting note content for URL: ${url}`);
try {
await this.initialize();
if (!this.page)
throw new Error('Page not initialized');
const actualURL = this.extractRedBookUrl(url);
await this.page.goto(actualURL);
let note = await (0, noteDetail_1.GetNoteDetail)(this.page);
note.url = url;
logger_1.default.info(`Successfully extracted note: ${note.title}`);
return note;
}
catch (error) {
logger_1.default.error('Error getting note content:', error);
throw error;
}
finally {
await this.cleanup();
}
}
async getNoteComments(url) {
logger_1.default.info(`Getting comments for URL: ${url}`);
try {
await this.initialize();
if (!this.page)
throw new Error('Page not initialized');
await this.page.goto(url);
// Wait for comments to load
logger_1.default.info('Waiting for comments to load');
await this.page.waitForSelector('[role="dialog"] [role="list"]');
// Extract comments
const comments = await this.page.evaluate(() => {
const items = document.querySelectorAll('[role="dialog"] [role="list"] [role="listitem"]');
const results = [];
items.forEach((item) => {
const author = item.querySelector('[data-testid="user-name"]')?.textContent?.trim() || '';
const content = item.querySelector('[data-testid="comment-content"]')?.textContent?.trim() || '';
const likes = parseInt(item.querySelector('[data-testid="likes-count"]')?.textContent || '0');
const time = item.querySelector('time')?.textContent?.trim() || '';
results.push({ author, content, likes, time });
});
return results;
});
logger_1.default.info(`Successfully extracted ${comments.length} comments`);
return comments;
}
catch (error) {
logger_1.default.error('Error getting note comments:', error);
throw error;
}
finally {
await this.cleanup();
}
}
async addNote(params) {
logger_1.default.info(`Publishing note with title: ${params.title}`);
try {
// Initialize browser and load cookies
logger_1.default.info('Initializing browser for publishing');
this.browser = await this.authManager.getBrowser();
if (!this.browser) {
throw new Error('Failed to initialize browser');
}
this.page = await this.browser.newPage();
// Load cookies if available
const cookies = await this.authManager.getCookies();
if (cookies.length > 0) {
logger_1.default.info(`Loading ${cookies.length} cookies for creator platform`);
// Add cookies for both xiaohongshu.com and creator.xiaohongshu.com
const creatorCookies = cookies.map(cookie => ({
...cookie,
domain: cookie.domain.startsWith('.') ? cookie.domain : `.${cookie.domain}`
}));
await this.page.context().addCookies(creatorCookies);
logger_1.default.info('Cookies loaded successfully');
}
else {
logger_1.default.warn('No cookies found, user may need to login first');
}
// Navigate to publish page
logger_1.default.info('Navigating to creator publish page');
await this.page.goto('https://creator.xiaohongshu.com/publish/publish', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
// Check if we need to login to creator platform
logger_1.default.info('Checking if login is required for creator platform');
await this.randomDelay(3, 5);
const currentUrl = this.page.url();
logger_1.default.info(`Current URL: ${currentUrl}`);
if (currentUrl.includes('/login')) {
logger_1.default.error('Redirected to login page');
return {
success: false,
message: '需要先登录创作者平台。请运行 rednote-mcp init 重新登录。'
};
}
// Check for actual login indicators instead of just text content
logger_1.default.info('Checking for login indicators on page');
try {
// 检查是否有具体的登录按钮或登录表单
const loginButton = await this.page.$('button:has-text("登录"), .login-btn, .login-button, input[type="submit"][value*="登录"]');
const loginForm = await this.page.$('form[class*="login"], .login-form, .login-container');
const qrCode = await this.page.$('.qrcode, .qr-code, [class*="qrcode"]');
if (loginButton || loginForm || qrCode) {
logger_1.default.error('Found login elements on page - login required');
return {
success: false,
message: '页面显示需要登录。请运行 rednote-mcp init 重新登录。'
};
}
logger_1.default.info('No login elements found, proceeding with publish flow');
}
catch (error) {
logger_1.default.warn('Error checking for login elements:', error);
// 如果检查失败,继续执行,让后续步骤来验证
}
// 判断上传类型并执行对应的上传流程
const hasVideos = params.videos && params.videos.length > 0;
const hasImages = params.images && params.images.length > 0;
if (hasVideos) {
// 视频上传:直接上传,不需要切换tab
logger_1.default.info('开始视频上传流程(直接上传)');
try {
// 等待页面加载完成
await this.randomDelay(2, 3);
// 直接查找视频上传区域,不点击tab
logger_1.default.info('查找视频上传区域');
// 等待视频上传区域出现
try {
await this.page.waitForSelector('input[type="file"], [class*="upload"], .upload-area, .dnd-area', {
timeout: 15000
});
logger_1.default.info('找到上传区域');
}
catch (error) {
logger_1.default.error('未找到上传区域');
return {
success: false,
message: '未找到视频上传区域'
};
}
// 查找文件上传输入框
const fileInput = await this.page.$('input[type="file"]');
if (!fileInput) {
logger_1.default.error('未找到文件上传输入框');
return {
success: false,
message: '未找到文件上传输入框'
};
}
// 上传视频文件
logger_1.default.info(`上传 ${params.videos.length} 个视频文件`);
try {
logger_1.default.info('设置视频文件...');
await fileInput.setInputFiles(params.videos);
logger_1.default.info('视频文件上传成功');
// 等待视频处理完成(视频处理通常需要更长时间)
await this.randomDelay(8, 12);
// 等待视频预览出现
try {
await this.page.waitForSelector('video, .video-preview, [class*="video"], [class*="preview"], .upload-success', {
timeout: 30000 // 视频处理需要更长时间
});
logger_1.default.info('视频预览加载完成');
}
catch (previewError) {
logger_1.default.warn('未找到视频预览,但继续执行');
// 额外等待视频处理
await this.randomDelay(5, 8);
}
}
catch (error) {
logger_1.default.error('视频上传失败:', error);
return {
success: false,
message: `视频上传失败: ${error instanceof Error ? error.message : '未知错误'}`
};
}
}
catch (error) {
logger_1.default.error('视频上传流程错误:', error);
return {
success: false,
message: '视频上传流程出错'
};
}
}
else if (hasImages) {
// 图文上传:需要切换到"上传图文"tab
logger_1.default.info('开始图文上传流程(切换到上传图文tab)');
try {
// 等待页面加载完成
await this.randomDelay(2, 3);
// 点击"上传图文"tab进行切换
logger_1.default.info('点击"上传图文"tab');
const tabClicked = await this.page.evaluate(() => {
// 查找所有包含"上传图文"文本的元素
const elements = Array.from(document.querySelectorAll('*'));
for (const el of elements) {
if (el.textContent?.includes('上传图文') && el.textContent.trim() === '上传图文') {
// 滚动到元素位置
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 强制点击
el.click();
return true;
}
}
return false;
});
if (tabClicked) {
logger_1.default.info('成功点击"上传图文"tab');
await this.randomDelay(3, 5); // 等待tab切换完成
}
else {
logger_1.default.warn('JavaScript点击失败,尝试Playwright点击');
// 备用方案:使用 Playwright 强制点击
try {
const tabElement = await this.page.locator('text=上传图文').first();
await tabElement.scrollIntoViewIfNeeded();
await tabElement.click({ force: true });
logger_1.default.info('Playwright强制点击成功');
await this.randomDelay(3, 5);
}
catch (clickError) {
logger_1.default.error('两种点击方式都失败');
return {
success: false,
message: '无法点击"上传图文"tab,请检查页面状态'
};
}
}
// 等待上传区域出现
logger_1.default.info('等待图文上传区域');
try {
await this.page.waitForSelector('input[type="file"], [class*="upload"], .upload-area, .dnd-area', {
timeout: 15000
});
logger_1.default.info('图文上传区域已找到');
}
catch (error) {
logger_1.default.error('切换tab后未找到上传区域');
return {
success: false,
message: '切换tab后未找到上传区域'
};
}
// 查找文件上传输入框
const fileInput = await this.page.$('input[type="file"]');
if (!fileInput) {
logger_1.default.error('切换tab后未找到文件上传输入框');
return {
success: false,
message: '切换tab后未找到文件上传输入框'
};
}
// 上传图片文件
logger_1.default.info(`上传 ${params.images.length} 个图片文件`);
try {
logger_1.default.info('设置图片文件...');
await fileInput.setInputFiles(params.images);
logger_1.default.info('图片文件上传成功');
// 等待图片处理完成
await this.randomDelay(3, 5);
// 等待图片预览出现
try {
await this.page.waitForSelector('img, .preview, [class*="preview"], [class*="image"], .upload-success', {
timeout: 15000
});
logger_1.default.info('图片预览加载完成');
}
catch (previewError) {
logger_1.default.warn('未找到图片预览,但继续执行');
// 额外等待图片处理
await this.randomDelay(3, 5);
}
}
catch (error) {
logger_1.default.error('图片上传失败:', error);
return {
success: false,
message: `图片上传失败: ${error instanceof Error ? error.message : '未知错误'}`
};
}
}
catch (error) {
logger_1.default.error('图文上传流程错误:', error);
return {
success: false,
message: '切换到图文上传页面时出错'
};
}
}
else {
// 纯文字发布:直接进入表单填写
logger_1.default.info('纯文字发布,等待页面加载');
await this.randomDelay(2, 3);
}
// Fill in the title
logger_1.default.info('Filling in title');
try {
// 等待一下确保图片上传后页面更新完成
await this.randomDelay(2, 3);
const titleSelectors = [
'input[placeholder*="标题"]',
'input[type="text"].d-text',
'input[type="text"]:first-of-type',
'input[placeholder*="请输入标题"]',
'input[placeholder*="添加标题"]'
];
let titleFilled = false;
for (const selector of titleSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
await this.page.fill(selector, params.title);
logger_1.default.info(`Title filled successfully with selector: ${selector}`);
titleFilled = true;
break;
}
catch {
continue;
}
}
if (!titleFilled) {
logger_1.default.error('Could not find title input with any selector');
return {
success: false,
message: '无法找到标题输入框,可能页面结构已更改'
};
}
}
catch (error) {
logger_1.default.error('Error filling title:', error);
return {
success: false,
message: '无法填写标题,可能页面结构已更改'
};
}
// Fill in the content
logger_1.default.info('Filling in content');
try {
const contentSelectors = [
'div[contenteditable="true"]:not([placeholder*="标题"])',
'div[contenteditable="true"]',
'textarea[placeholder*="描述"]',
'textarea[placeholder*="请输入正文"]',
'textarea[placeholder*="添加描述"]',
'textarea'
];
let contentFilled = false;
for (const selector of contentSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
await this.page.fill(selector, params.content);
logger_1.default.info(`Content filled successfully with selector: ${selector}`);
contentFilled = true;
break;
}
catch {
continue;
}
}
if (!contentFilled) {
logger_1.default.error('Could not find content input with any selector');
return {
success: false,
message: '无法找到内容输入框,可能页面结构已更改'
};
}
}
catch (error) {
logger_1.default.error('Error filling content:', error);
return {
success: false,
message: '无法填写内容,可能页面结构已更改'
};
}
// Add tags if provided
if (params.tags && params.tags.length > 0) {
logger_1.default.info(`Adding ${params.tags.length} tags`);
try {
// 尝试找到标签输入框
const tagSelector = 'input[placeholder*="标签"], input[placeholder*="话题"], .tag-input input, [data-testid="tag-input"]';
const tagInput = await this.page.$(tagSelector);
if (tagInput) {
for (const tag of params.tags) {
await tagInput.fill(`#${tag}`);
await this.page.keyboard.press('Enter');
await this.randomDelay(0.5, 1);
}
logger_1.default.info('Tags added successfully');
}
else {
logger_1.default.warn('Tag input not found, skipping tags');
}
}
catch (error) {
logger_1.default.error('Error adding tags:', error);
// 不因为标签失败而终止发布
}
}
// Set privacy if specified
if (params.isPrivate !== undefined) {
logger_1.default.info(`Setting privacy to: ${params.isPrivate ? 'private' : 'public'}`);
try {
const privacySelector = params.isPrivate
? '[data-testid="private-radio"], input[value="private"], .privacy-private'
: '[data-testid="public-radio"], input[value="public"], .privacy-public';
const privacyOption = await this.page.$(privacySelector);
if (privacyOption) {
await privacyOption.click();
logger_1.default.info('Privacy setting applied');
}
else {
logger_1.default.warn('Privacy setting not found, using default');
}
}
catch (error) {
logger_1.default.error('Error setting privacy:', error);
// 不因为隐私设置失败而终止发布
}
}
// Wait a bit before publishing
await this.randomDelay(2, 3);
// 检查是否需要人工验证
if (params.manualReview) {
logger_1.default.info('人工验证模式:内容已填写完成,等待用户手动发布');
console.log('\n🔍 人工验证模式已启用');
console.log('📝 所有内容已自动填写完成,包括:');
console.log(' ✅ 标题');
console.log(' ✅ 内容');
if (params.images && params.images.length > 0) {
console.log(` ✅ 图片 (${params.images.length} 个)`);
}
if (params.videos && params.videos.length > 0) {
console.log(` ✅ 视频 (${params.videos.length} 个)`);
}
if (params.tags && params.tags.length > 0) {
console.log(` ✅ 标签 (${params.tags.join(', ')})`);
}
if (params.isPrivate !== undefined) {
console.log(` ✅ 隐私设置 (${params.isPrivate ? '私密' : '公开'})`);
}
console.log('\n👆 请手动检查页面内容,确认无误后点击"发布"按钮');
console.log('⏳ 程序将等待30秒,如需更长时间请关闭浏览器中断程序');
console.log('🔍 您可以在这个时间内:');
console.log(' - 检查上传的图片/视频是否正确');
console.log(' - 修改或完善文案内容');
console.log(' - 调整标签和隐私设置');
console.log(' - 手动点击发布按钮');
// 等待30秒让用户进行人工验证和发布
await new Promise(resolve => setTimeout(resolve, 30000));
// 检查是否已经发布成功(用户可能已经手动点击了发布)
logger_1.default.info('检查用户是否已手动发布');
const successIndicators = [
'.success-message',
'.publish-success',
'text=发布成功',
'text=已发布',
'text=发布中',
'text=审核中',
'text=上传成功',
'[data-testid="success-message"]',
'.toast-success',
'.message-success'
];
let userPublished = false;
let successMessage = '';
for (const indicator of successIndicators) {
try {
const element = await this.page.$(indicator);
if (element) {
const text = await element.textContent();
if (text && text.trim()) {
userPublished = true;
successMessage = text.trim();
logger_1.default.info(`User manually published: ${successMessage}`);
break;
}
}
}
catch {
continue;
}
}
if (userPublished) {
return {
success: true,
message: `人工验证发布成功: ${successMessage}`
};
}
else {
// 用户没有手动发布,返回等待状态
return {
success: true,
message: '内容已准备完成,等待人工验证发布。请在浏览器中检查内容并手动点击发布按钮。'
};
}
}
// 自动发布模式:点击发布按钮
logger_1.default.info('Clicking publish button');
try {
const publishSelectors = [
'button.publishBtn',
'button:has-text("发布")',
'button:has-text("立即发布")',
'button[type="submit"]',
'.publish-btn',
'.submit-btn'
];
let publishClicked = false;
for (const selector of publishSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
await this.page.click(selector);
logger_1.default.info(`Publish button clicked with selector: ${selector}`);
publishClicked = true;
break;
}
catch {
continue;
}
}
if (!publishClicked) {
logger_1.default.error('Could not find publish button with any selector');
return {
success: false,
message: '无法找到发布按钮,可能页面结构已更改'
};
}
// Wait a bit for the publish process
await this.randomDelay(3, 5);
// Check for success indicators with more comprehensive detection
logger_1.default.info('Checking for publish success indicators');
const successIndicators = [
'.success-message',
'.publish-success',
'text=发布成功',
'text=已发布',
'text=发布中',
'text=审核中',
'text=上传成功',
'[data-testid="success-message"]',
'.toast-success',
'.message-success'
];
let publishSuccess = false;
let successMessage = '';
// 首先尝试检测成功指示器
for (const indicator of successIndicators) {
try {
const element = await this.page.waitForSelector(indicator, { timeout: 3000 });
if (element) {
const text = await element.textContent();
if (text && text.trim()) {
publishSuccess = true;
successMessage = text.trim();
logger_1.default.info(`Publish success confirmed with text: ${successMessage}`);
break;
}
}
}
catch {
// 继续尝试下一个指示器
}
}
// 如果没找到明确的成功指示器,检查当前页面状态
if (!publishSuccess) {
logger_1.default.info('No explicit success indicator found, checking page state');
try {
// 检查URL是否变化(可能跳转到了发布成功页面)
const currentUrl = this.page.url();
if (currentUrl.includes('success') || currentUrl.includes('published') || currentUrl !== 'https://creator.xiaohongshu.com/publish/publish') {
logger_1.default.info(`URL changed to: ${currentUrl}, likely success`);
publishSuccess = true;
successMessage = '发布成功(根据页面跳转判断)';
}
// 检查是否回到了发布页面的初始状态(说明发布完成)
if (!publishSuccess) {
const publishButton = await this.page.$('button:has-text("发布"), button:has-text("立即发布")');
if (publishButton) {
const isEnabled = await publishButton.isEnabled();
if (isEnabled) {
logger_1.default.info('Publish button is enabled again, likely success');
publishSuccess = true;
successMessage = '发布成功(发布按钮重新可用)';
}
}
}
// 检查是否有新的发布任务出现在列表中
if (!publishSuccess) {
const draftElements = await this.page.$$('.draft-item, .note-item, [class*="publish-list"]');
if (draftElements.length > 0) {
logger_1.default.info('Found draft/note items, likely success');
publishSuccess = true;
successMessage = '发布成功(在发布列表中找到新项目)';
}
}
}
catch (error) {
logger_1.default.warn('Error checking page state:', error);
}
}
// 如果仍然没有确认成功,再检查错误信息
if (!publishSuccess) {
logger_1.default.info('Checking for error messages');
const errorElements = await this.page.$$('.error-message, .publish-error, [class*="error"], .toast-error');
if (errorElements.length > 0) {
for (const errorElement of errorElements) {
const errorText = await errorElement.textContent();
if (errorText && errorText.trim() && !errorText.includes('请刷新试试')) {
// 如果找到了实际的错误信息(不是"请刷新试试"这种模糊信息)
logger_1.default.error(`Found actual error: ${errorText.trim()}`);
return {
success: false,
message: `发布失败: ${errorText.trim()}`
};
}
}
}
// 如果没有找到实际错误,但也没有成功指示器
// 等待更长时间,可能是网络较慢
logger_1.default.info('No clear success or error, waiting longer...');
await this.randomDelay(5, 8);
// 再次检查成功指示器
for (const indicator of successIndicators) {
try {
const element = await this.page.$(indicator);
if (element) {
const text = await element.textContent();
if (text && text.trim()) {
publishSuccess = true;
successMessage = text.trim();
logger_1.default.info(`Success found after extended wait: ${successMessage}`);
break;
}
}
}
catch {
continue;
}
}
}
// 最终结果判断
if (publishSuccess) {
return {
success: true,
message: successMessage || '笔记发布成功!'
};
}
else {
// 如果实在无法确定状态,提供更有用的信息
logger_1.default.warn('Cannot determine publish status definitively');
return {
success: false,
message: '发布状态不明确,请手动检查小红书是否发布成功。如果看到"请刷新试试"提示,通常表示正在处理中,请稍等片刻后查看。'
};
}
}
catch (error) {
logger_1.default.error('Error clicking publish button:', error);
return {
success: false,
message: `发布失败: ${error instanceof Error ? error.message : '未知错误'}`
};
}
}
catch (error) {
logger_1.default.error('Error publishing note:', error);
return {
success: false,
message: `发布失败: ${error instanceof Error ? error.message : '未知错误'}`
};
}
finally {
// 如果是人工验证模式且没有发布成功,延迟清理以保持浏览器打开
if (params.manualReview) {
logger_1.default.info('人工验证模式:延迟清理浏览器资源,保持页面打开');
// 在人工验证模式下,不立即清理,让用户有时间操作
// 用户可以手动关闭浏览器或等待进程结束
setTimeout(() => {
this.cleanup().catch(err => logger_1.default.error('Delayed cleanup error:', err));
}, 60000); // 1分钟后自动清理
}
else {
// 自动模式下立即清理
await this.cleanup();
}
}
}
/**
* Wait for a random duration between min and max seconds
* @param min Minimum seconds to wait
* @param max Maximum seconds to wait
*/
async randomDelay(min, max) {
const delay = Math.random() * (max - min) + min;
logger_1.default.debug(`Adding random delay of ${delay.toFixed(2)} seconds`);
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
}
}
exports.RedNoteTools = RedNoteTools;