UNPKG

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
"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;