Skip to main content
Glama
post-manager.js25.7 kB
/** * 内容发布管理器 * 管理小红书内容的发布、搜索、推荐等功能 */ import { logger } from '../utils/logger.js'; import { query, transaction } from '../database/index.js'; import { BrowserManager } from '../browser/browser-manager.js'; import { AccountManager } from './account-manager.js'; import { TaskManager } from './task-manager.js'; import { v4 as uuidv4 } from 'uuid'; /** * 内容发布管理器类 */ export class PostManager { constructor() { this.browserManager = new BrowserManager(); this.accountManager = new AccountManager(); this.taskManager = new TaskManager(); this.postingQueue = new Map(); // 发布队列 } /** * 搜索笔记 * @param {Object} searchParams - 搜索参数 * @returns {Promise<Object>} 搜索结果 */ async searchPosts(searchParams) { const { keyword, category = 'all', sort = 'relevance', page = 1, pageSize = 20, accountId } = searchParams; try { // 验证参数 if (!keyword || keyword.trim().length === 0) { throw new Error('搜索关键词不能为空'); } // 获取账号会话 let page; if (accountId) { const session = this.accountManager.getLoginSession(accountId); if (!session) { throw new Error('账号未登录'); } page = await this.browserManager.getPage({ accountId, cookies: session.cookies }); } else { // 使用匿名浏览器 page = await this.browserManager.getPage(); } // 导航到搜索页面 const searchUrl = `https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(keyword)}&type=${category}`; await page.goto(searchUrl, { waitUntil: 'networkidle' }); // 等待搜索结果加载 await page.waitForSelector('.search-result-container, .feeds-container, [data-testid="search-results"]', { timeout: 10000 }); // 获取搜索结果 const posts = await page.evaluate((sortType) => { const results = []; // 查找笔记元素 const postElements = document.querySelectorAll( '.note-item, .feed-item, [data-testid="note-card"], .search-item' ); postElements.forEach((element, index) => { try { // 提取笔记信息 const titleElement = element.querySelector('.title, .note-title, [data-testid="title"]'); const descElement = element.querySelector('.desc, .description, [data-testid="description"]'); const authorElement = element.querySelector('.author, .user-name, [data-testid="author"]'); const likeElement = element.querySelector('.like-count, .likes, [data-testid="likes"]'); const collectElement = element.querySelector('.collect-count, .collects, [data-testid="collects"]'); const commentElement = element.querySelector('.comment-count, .comments, [data-testid="comments"]'); const imageElement = element.querySelector('img, .cover-image, [data-testid="cover"]'); const linkElement = element.querySelector('a, [data-testid="link"]'); const post = { id: element.getAttribute('data-note-id') || `search_${index}`, title: titleElement ? titleElement.textContent.trim() : '', description: descElement ? descElement.textContent.trim() : '', author: authorElement ? authorElement.textContent.trim() : '', likes: likeElement ? parseInt(likeElement.textContent.trim()) || 0 : 0, collects: collectElement ? parseInt(collectElement.textContent.trim()) || 0 : 0, comments: commentElement ? parseInt(commentElement.textContent.trim()) || 0 : 0, imageUrl: imageElement ? imageElement.src : '', postUrl: linkElement ? linkElement.href : '', tags: [], createdTime: new Date().toISOString() }; // 提取标签 const tagElements = element.querySelectorAll('.tag, [data-testid="tag"]'); post.tags = Array.from(tagElements).map(tag => tag.textContent.trim()); results.push(post); } catch (error) { console.error('解析笔记失败:', error); } }); // 根据排序类型排序 if (sortType === 'likes') { results.sort((a, b) => b.likes - a.likes); } else if (sortType === 'comments') { results.sort((a, b) => b.comments - a.comments); } else if (sortType === 'time') { // 按时间排序(这里假设时间信息可用) } return results; }, sort); // 保存搜索记录 await this.saveSearchHistory({ keyword, category, accountId, resultCount: posts.length }); // 分页处理 const total = posts.length; const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedPosts = posts.slice(startIndex, endIndex); logger.info(`搜索笔记成功: ${keyword}, 找到 ${total} 条结果`); return { data: paginatedPosts, pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) }, searchInfo: { keyword, category, sort, searchTime: new Date().toISOString() } }; } catch (error) { logger.error('搜索笔记失败:', error); throw new Error('搜索笔记失败: ' + error.message); } } /** * 获取推荐内容 * @param {Object} params - 参数 * @returns {Promise<Object>} 推荐内容 */ async getRecommendations(params) { const { category = 'all', accountId, page = 1, pageSize = 20 } = params; try { // 获取账号会话 let page; if (accountId) { const session = this.accountManager.getLoginSession(accountId); if (!session) { throw new Error('账号未登录'); } page = await this.browserManager.getPage({ accountId, cookies: session.cookies }); } else { // 使用匿名浏览器 page = await this.browserManager.getPage(); } // 导航到首页 await page.goto('https://www.xiaohongshu.com', { waitUntil: 'networkidle' }); // 等待推荐内容加载 await page.waitForSelector('.recommend-container, .feeds-container, [data-testid="recommendations"]', { timeout: 10000 }); // 获取推荐内容 const recommendations = await page.evaluate((categoryType) => { const results = []; // 查找推荐内容元素 const feedElements = document.querySelectorAll( '.feed-item, .recommend-item, [data-testid="feed-card"], .note-card' ); feedElements.forEach((element, index) => { try { // 提取内容信息 const titleElement = element.querySelector('.title, .feed-title, [data-testid="title"]'); const descElement = element.querySelector('.desc, .feed-desc, [data-testid="description"]'); const authorElement = element.querySelector('.author, .user-name, [data-testid="author"]'); const likeElement = element.querySelector('.like-count, .likes, [data-testid="likes"]'); const collectElement = element.querySelector('.collect-count, .collects, [data-testid="collects"]'); const commentElement = element.querySelector('.comment-count, .comments, [data-testid="comments"]'); const imageElement = element.querySelector('img, .cover-image, [data-testid="cover"]'); const linkElement = element.querySelector('a, [data-testid="link"]'); const categoryElement = element.querySelector('.category, [data-testid="category"]'); // 过滤指定分类 if (categoryType !== 'all' && categoryElement) { const contentCategory = categoryElement.textContent.trim(); if (contentCategory !== categoryType) { return; } } const post = { id: element.getAttribute('data-note-id') || `recommend_${index}`, title: titleElement ? titleElement.textContent.trim() : '', description: descElement ? descElement.textContent.trim() : '', author: authorElement ? authorElement.textContent.trim() : '', likes: likeElement ? parseInt(likeElement.textContent.trim()) || 0 : 0, collects: collectElement ? parseInt(collectElement.textContent.trim()) || 0 : 0, comments: commentElement ? parseInt(commentElement.textContent.trim()) || 0 : 0, imageUrl: imageElement ? imageElement.src : '', postUrl: linkElement ? linkElement.href : '', category: categoryElement ? categoryElement.textContent.trim() : '', tags: [], isRecommended: true, createdTime: new Date().toISOString() }; // 提取标签 const tagElements = element.querySelectorAll('.tag, [data-testid="tag"]'); post.tags = Array.from(tagElements).map(tag => tag.textContent.trim()); results.push(post); } catch (error) { console.error('解析推荐内容失败:', error); } }); return results; }, category); // 分页处理 const total = recommendations.length; const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedRecommendations = recommendations.slice(startIndex, endIndex); logger.info(`获取推荐内容成功: 找到 ${total} 条推荐`); return { data: paginatedRecommendations, pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) }, category, recommendTime: new Date().toISOString() }; } catch (error) { logger.error('获取推荐内容失败:', error); throw new Error('获取推荐内容失败: ' + error.message); } } /** * 发布笔记 * @param {Object} postData - 发布数据 * @returns {Promise<Object>} 发布结果 */ async createPost(postData) { const { accountId, title, content, images, video, tags = [], topic, type = 'image', scheduledTime, publishNow = false } = postData; try { // 验证参数 if (!accountId) { throw new Error('账号ID不能为空'); } if (!title || title.trim().length === 0) { throw new Error('标题不能为空'); } if (type === 'image' && (!images || images.length === 0)) { throw new Error('图片笔记至少需要一张图片'); } if (type === 'video' && !video) { throw new Error('视频笔记需要视频文件'); } // 检查账号状态 const account = await query( 'SELECT * FROM idea_xiaohongshu_accounts WHERE id = ? AND status = "active"', [accountId] ); if (account.length === 0) { throw new Error('账号不存在或不可用'); } const accountData = account[0]; // 检查是否已登录 const session = this.accountManager.getLoginSession(accountId); if (!session) { throw new Error('账号未登录'); } // 内容审核 const auditResult = await this.contentAudit({ title, content, tags, topic }); if (!auditResult.passed) { throw new Error(`内容审核未通过: ${auditResult.reason}`); } // 创建发布任务 const taskId = uuidv4(); const postId = uuidv4(); // 保存到数据库 const result = await query( `INSERT INTO idea_xiaohongshu_posts (id, account_id, title, content, type, status, images_data, video_data, tags, topic, scheduled_time) VALUES (?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?)`, [postId, accountId, title, content, type, JSON.stringify(images), JSON.stringify(video), JSON.stringify(tags), topic, scheduledTime] ); if (publishNow || !scheduledTime) { // 立即发布 const publishResult = await this.publishPost(postId); return publishResult; } else { // 添加到任务队列 await this.taskManager.createTask({ taskType: 'publish_post', accountId, taskData: { postId, title, content, images, video, tags, topic, type }, scheduledTime, priority: 1 }); logger.info(`创建发布任务成功: ${title} (ID: ${postId})`); return { success: true, postId, status: 'scheduled', scheduledTime, message: '发布任务已创建,将在指定时间发布' }; } } catch (error) { logger.error('创建发布任务失败:', error); throw error; } } /** * 发布笔记到小红书 * @param {string} postId - 笔记ID * @returns {Promise<Object>} 发布结果 */ async publishPost(postId) { try { // 获取笔记信息 const post = await query( 'SELECT * FROM idea_xiaohongshu_posts WHERE id = ?', [postId] ); if (post.length === 0) { throw new Error('笔记不存在'); } const postData = post[0]; // 检查账号状态 const account = await query( 'SELECT * FROM idea_xiaohongshu_accounts WHERE id = ? AND status = "active"', [postData.account_id] ); if (account.length === 0) { throw new Error('账号不存在或不可用'); } const accountData = account[0]; // 检查是否已登录 const session = this.accountManager.getLoginSession(postData.account_id); if (!session) { throw new Error('账号未登录'); } // 获取浏览器页面 const page = await this.browserManager.getPage({ accountId: postData.account_id, cookies: session.cookies }); // 导航到发布页面 await page.goto('https://www.xiaohongshu.com/publish', { waitUntil: 'networkidle' }); // 等待发布页面加载 await page.waitForSelector('.publish-container, .upload-container, [data-testid="publish-form"]', { timeout: 10000 }); // 发布内容 let publishResult; if (postData.type === 'image') { publishResult = await this.publishImagePost(page, postData); } else if (postData.type === 'video') { publishResult = await this.publishVideoPost(page, postData); } else { throw new Error('不支持的笔记类型'); } if (publishResult.success) { // 更新笔记状态 await query( `UPDATE idea_xiaohongshu_posts SET status = 'published', post_id = ?, published_time = NOW() WHERE id = ?`, [publishResult.postId, postId] ); logger.info(`发布笔记成功: ${postData.title} (ID: ${postId})`); return { success: true, postId, xiaohongshuPostId: publishResult.postId, publishedTime: new Date().toISOString(), message: '发布成功' }; } else { // 更新失败状态 await query( 'UPDATE idea_xiaohongshu_posts SET status = "failed" WHERE id = ?', [postId] ); throw new Error(`发布失败: ${publishResult.error}`); } } catch (error) { logger.error('发布笔记失败:', error); // 更新失败状态 await query( 'UPDATE idea_xiaohongshu_posts SET status = "failed" WHERE id = ?', [postId] ); throw error; } } /** * 发布图文笔记 * @param {Object} page - 浏览器页面 * @param {Object} postData - 笔记数据 * @returns {Promise<Object>} 发布结果 */ async publishImagePost(page, postData) { try { const { title, content, images, tags, topic } = postData; // 上传图片 const imagesData = JSON.parse(images); if (imagesData && imagesData.length > 0) { const fileInput = await page.$('input[type="file"], [data-testid="image-upload"]'); if (fileInput) { // 处理图片上传逻辑 // 这里需要根据实际的文件路径或URL来处理 logger.info(`准备上传 ${imagesData.length} 张图片`); } } // 填写标题 const titleInput = await page.$('input[placeholder*="标题"], textarea[placeholder*="标题"], [data-testid="title-input"]'); if (titleInput) { await titleInput.fill(title); } // 填写内容 const contentInput = await page.$('textarea[placeholder*="内容"], [data-testid="content-input"]'); if (contentInput) { await contentInput.fill(content); } // 添加标签 if (tags && tags.length > 0) { const tagInput = await page.$('input[placeholder*="标签"], [data-testid="tag-input"]'); if (tagInput) { for (const tag of tags) { await tagInput.fill(tag); await page.keyboard.press('Enter'); await page.waitForTimeout(500); // 等待标签添加 } } } // 添加话题 if (topic) { const topicInput = await page.$('input[placeholder*="话题"], [data-testid="topic-input"]'); if (topicInput) { await topicInput.fill(topic); } } // 点击发布按钮 const publishButton = await page.$('button[type="submit"], .publish-btn, [data-testid="publish-btn"]'); if (publishButton) { await publishButton.click(); // 等待发布完成 try { await page.waitForSelector('.publish-success, .success-message, [data-testid="publish-success"]', { timeout: 30000 }); // 获取发布后的笔记ID const postId = await page.evaluate(() => { const url = window.location.href; const match = url.match(/\/discovery\/item\/([a-zA-Z0-9]+)/); return match ? match[1] : null; }); return { success: true, postId }; } catch (timeoutError) { // 检查是否有错误信息 const errorElement = await page.$('.error-message, .publish-error, [data-testid="error-message"]'); if (errorElement) { const errorText = await errorElement.textContent(); return { success: false, error: errorText }; } throw new Error('发布超时'); } } else { throw new Error('未找到发布按钮'); } } catch (error) { logger.error('发布图文笔记失败:', error); return { success: false, error: error.message }; } } /** * 发布视频笔记 * @param {Object} page - 浏览器页面 * @param {Object} postData - 笔记数据 * @returns {Promise<Object>} 发布结果 */ async publishVideoPost(page, postData) { try { const { title, content, video, tags, topic } = postData; // 切换到视频发布模式 const videoTab = await page.$('.video-tab, [data-testid="video-tab"]'); if (videoTab) { await videoTab.click(); await page.waitForTimeout(1000); // 等待切换 } // 上传视频 const videoData = JSON.parse(video); if (videoData) { const fileInput = await page.$('input[type="file"], [data-testid="video-upload"]'); if (fileInput) { // 处理视频上传逻辑 logger.info('准备上传视频'); } } // 填写标题 const titleInput = await page.$('input[placeholder*="标题"], textarea[placeholder*="标题"], [data-testid="title-input"]'); if (titleInput) { await titleInput.fill(title); } // 填写内容 const contentInput = await page.$('textarea[placeholder*="内容"], [data-testid="content-input"]'); if (contentInput) { await contentInput.fill(content); } // 添加标签 if (tags && tags.length > 0) { const tagInput = await page.$('input[placeholder*="标签"], [data-testid="tag-input"]'); if (tagInput) { for (const tag of tags) { await tagInput.fill(tag); await page.keyboard.press('Enter'); await page.waitForTimeout(500); } } } // 添加话题 if (topic) { const topicInput = await page.$('input[placeholder*="话题"], [data-testid="topic-input"]'); if (topicInput) { await topicInput.fill(topic); } } // 点击发布按钮 const publishButton = await page.$('button[type="submit"], .publish-btn, [data-testid="publish-btn"]'); if (publishButton) { await publishButton.click(); // 等待发布完成 try { await page.waitForSelector('.publish-success, .success-message, [data-testid="publish-success"]', { timeout: 60000 // 视频发布可能需要更长时间 }); // 获取发布后的笔记ID const postId = await page.evaluate(() => { const url = window.location.href; const match = url.match(/\/discovery\/item\/([a-zA-Z0-9]+)/); return match ? match[1] : null; }); return { success: true, postId }; } catch (timeoutError) { const errorElement = await page.$('.error-message, .publish-error, [data-testid="error-message"]'); if (errorElement) { const errorText = await errorElement.textContent(); return { success: false, error: errorText }; } throw new Error('发布超时'); } } else { throw new Error('未找到发布按钮'); } } catch (error) { logger.error('发布视频笔记失败:', error); return { success: false, error: error.message }; } } /** * 内容审核 * @param {Object} content - 内容数据 * @returns {Promise<Object>} 审核结果 */ async contentAudit(content) { const { title, content: text, tags, topic } = content; try { // 敏感词检测 const sensitiveWords = [ '赌博', '色情', '毒品', '暴力', '恐怖主义', '政治敏感', '违法', '犯罪', '诈骗', '传销' ]; const fullText = `${title} ${text} ${tags.join(' ')} ${topic || ''}`.toLowerCase(); for (const word of sensitiveWords) { if (fullText.includes(word)) { return { passed: false, reason: `内容包含敏感词: ${word}` }; } } // 内容长度检查 if (title.length > 100) { return { passed: false, reason: '标题长度超过限制(100字符)' }; } if (text.length > 5000) { return { passed: false, reason: '内容长度超过限制(5000字符)' }; } if (tags.length > 10) { return { passed: false, reason: '标签数量超过限制(10个)' }; } return { passed: true, reason: '' }; } catch (error) { logger.error('内容审核失败:', error); return { passed: false, reason: '审核过程出错' }; } } /** * 保存搜索历史 * @param {Object} searchData - 搜索数据 */ async saveSearchHistory(searchData) { const { keyword, category, accountId, resultCount } = searchData; try { // 这里可以实现搜索历史的保存逻辑 // 例如保存到数据库或缓存中 logger.debug(`保存搜索历史: ${keyword} (${resultCount} 结果)`); } catch (error) { logger.error('保存搜索历史失败:', error); } } /** * 获取发布队列状态 * @returns {Object} 队列状态 */ getQueueStatus() { return { total: this.postingQueue.size, queues: Array.from(this.postingQueue.entries()).map(([key, item]) => ({ key, accountId: item.accountId, title: item.title, status: item.status, createdAt: item.createdAt })) }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/billyangbc/xiaohongshu-mcp-nodejs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server