wechat_publish_article
Publish articles to WeChat Official Accounts using Markdown format, converting content to HTML with image handling and preview options.
Instructions
将文章发布到微信公众号,支持Markdown格式
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | 文章标题 | |
| content | Yes | Markdown格式的文章内容 | |
| author | Yes | 作者名称 | |
| appId | Yes | 微信公众号AppID | |
| appSecret | Yes | 微信公众号AppSecret | |
| coverImagePath | No | 封面图片路径 | |
| previewMode | No | 是否为预览模式 | |
| previewOpenId | No | 预览用户OpenID |
Implementation Reference
- src/tools/wechat-publisher.js:16-163 (handler)Core handler function WeChatPublisher.publish() that implements the complete tool logic: parameter validation, Markdown to WeChat HTML conversion, automatic cover image generation/upload, article publishing or previewing via WeChatAPI.static async publish(params) { const startTime = Date.now(); try { // 详细记录调用参数(隐藏敏感信息) const logParams = { title: params.title, author: params.author, contentLength: params.content ? params.content.length : 0, contentPreview: params.content ? params.content.substring(0, 100) + '...' : '', appId: params.appId ? params.appId.substring(0, 8) + '***' : 'undefined', appSecret: params.appSecret ? '***已提供***' : 'undefined', coverImagePath: params.coverImagePath || 'undefined', previewMode: params.previewMode || false, previewOpenId: params.previewOpenId || 'undefined' }; logger.info('=== MCP调用开始 ==='); logger.info('调用参数详情', logParams); logger.info('开始发布流程', { title: params.title }); // 1. 参数验证 const validation = validatePublishParams(params); if (!validation.valid) { throw new Error(`参数验证失败: ${validation.errors.join(', ')}`); } const { title, content, author, appId, appSecret, coverImagePath, previewMode = false, previewOpenId } = params; // 2. 初始化微信API logger.debug('初始化微信API'); const wechatAPI = new WeChatAPI(appId, appSecret); // 3. 转换Markdown为微信HTML logger.debug('转换Markdown内容'); const htmlContent = MarkdownConverter.convertToWeChatHTML(content); logger.debug('Markdown转换完成', { originalLength: content.length, htmlLength: htmlContent.length }); // 4. 处理封面图 - 如果没有提供封面图,则自动生成 let thumbMediaId = null; let coverPath = coverImagePath; if (!coverPath) { // 自动生成封面图 logger.info('未提供封面图,正在根据文章内容自动生成封面图...'); coverPath = await WeChatPublisher.generateCoverImage(title, content); } if (coverPath) { try { logger.debug('开始上传封面图', { path: coverPath }); thumbMediaId = await wechatAPI.uploadCoverImage(coverPath); logger.info('封面图上传成功', { mediaId: thumbMediaId }); // 如果是自动生成的封面图,上传后删除临时文件 if (!coverImagePath && coverPath) { try { const fs = await import('fs/promises'); await fs.unlink(coverPath); logger.debug('临时封面图文件已清理', { coverPath }); } catch (cleanupError) { logger.warn('清理临时封面图文件失败', { error: cleanupError.message }); } } } catch (error) { logger.warn('封面图上传失败,将继续发布', { error: error.message }); // 不抛出错误,继续发布流程 } } // 5. 发布或预览文章 let result; if (previewMode) { if (!previewOpenId) { throw new Error('预览模式需要提供previewOpenId参数'); } logger.debug('开始预览文章', { previewOpenId }); result = await wechatAPI.previewArticle({ title, content: htmlContent, author, thumbMediaId, previewOpenId }); } else { logger.debug('开始正式发布文章'); result = await wechatAPI.publishArticle({ title, content: htmlContent, author, thumbMediaId }); } const executionTime = Date.now() - startTime; logger.info(`文章${previewMode ? '预览' : '发布'}成功`, { ...result, executionTime: `${executionTime}ms` }); // 6. 构建成功响应 const successMessage = this.buildSuccessMessage({ title, author, result, previewMode, executionTime, thumbMediaId }); return { content: [{ type: "text", text: successMessage }] }; } catch (error) { const executionTime = Date.now() - startTime; logger.error('发布流程失败', { error: error.message, executionTime: `${executionTime}ms`, stack: error.stack }); return { content: [{ type: "text", text: this.buildErrorMessage(error, params) }], isError: true }; } }
- src/server.js:27-71 (registration)MCP tool registration for 'wechat_publish_article' using server.registerTool(), including input schema and async handler that invokes WeChatPublisher.publish().server.registerTool( "wechat_publish_article", { description: "将文章发布到微信公众号,支持Markdown格式", inputSchema: { title: z.string().describe("文章标题"), content: z.string().describe("Markdown格式的文章内容"), author: z.string().describe("作者名称"), appId: z.string().describe("微信公众号AppID"), appSecret: z.string().describe("微信公众号AppSecret"), coverImagePath: z.string().optional().describe("封面图片路径"), previewMode: z.boolean().default(false).describe("是否为预览模式"), previewOpenId: z.string().optional().describe("预览用户OpenID") } }, async (params) => { const { title, content, author, appId, appSecret, coverImagePath, previewMode, previewOpenId } = params; logger.info(`Publishing article: ${title}`); try { // 调用实际的发布逻辑 const result = await WeChatPublisher.publish({ title, content, author, appId, appSecret, coverImagePath, previewMode, previewOpenId }); return result; } catch (error) { logger.error(`发布失败: ${error.message}`); return { content: [{ type: "text", text: `❌ 发布失败: ${error.message}` }], isError: true }; } } );
- src/server.js:30-40 (schema)Zod-based input schema defining parameters for the tool: title, content, author, appId, appSecret, coverImagePath, previewMode, previewOpenId.description: "将文章发布到微信公众号,支持Markdown格式", inputSchema: { title: z.string().describe("文章标题"), content: z.string().describe("Markdown格式的文章内容"), author: z.string().describe("作者名称"), appId: z.string().describe("微信公众号AppID"), appSecret: z.string().describe("微信公众号AppSecret"), coverImagePath: z.string().optional().describe("封面图片路径"), previewMode: z.boolean().default(false).describe("是否为预览模式"), previewOpenId: z.string().optional().describe("预览用户OpenID") }
- src/utils/validator.js:11-89 (schema)Comprehensive parameter validation logic validatePublishParams() enforcing types, lengths, formats (e.g., AppID starts with 'wx', length 18), and business rules (e.g., previewMode requires previewOpenId).function validatePublishParams(params) { const errors = []; // 必需参数检查 if (!params.title || typeof params.title !== 'string' || params.title.trim() === '') { errors.push('title参数是必需的,且不能为空字符串'); } if (!params.content || typeof params.content !== 'string' || params.content.trim() === '') { errors.push('content参数是必需的,且不能为空字符串'); } if (!params.appId || typeof params.appId !== 'string' || params.appId.trim() === '') { errors.push('appId参数是必需的,且不能为空字符串'); } if (!params.appSecret || typeof params.appSecret !== 'string' || params.appSecret.trim() === '') { errors.push('appSecret参数是必需的,且不能为空字符串'); } // 可选参数类型检查 if (params.author && typeof params.author !== 'string') { errors.push('author参数必须是字符串类型'); } if (params.coverImagePath && typeof params.coverImagePath !== 'string') { errors.push('coverImagePath参数必须是字符串类型'); } if (params.previewMode !== undefined && typeof params.previewMode !== 'boolean') { errors.push('previewMode参数必须是布尔值类型'); } if (params.previewOpenId && typeof params.previewOpenId !== 'string') { errors.push('previewOpenId参数必须是字符串类型'); } // 业务规则验证 if (params.title && params.title.length > 64) { errors.push('标题长度不能超过64个字符'); } if (params.author && params.author.length > 8) { errors.push('作者名称长度不能超过8个字符'); } if (params.content && params.content.length > 200000) { errors.push('文章内容长度不能超过200,000个字符'); } // AppID格式验证 if (params.appId && !params.appId.startsWith('wx')) { errors.push('AppID格式错误,应该以"wx"开头'); } if (params.appId && params.appId.length !== 18) { errors.push('AppID长度应该为18个字符'); } // AppSecret格式验证 if (params.appSecret && params.appSecret.length !== 32) { errors.push('AppSecret长度应该为32个字符'); } // 预览模式验证 if (params.previewMode === true && !params.previewOpenId) { errors.push('预览模式下必须提供previewOpenId参数'); } // OpenID格式验证(如果提供了) if (params.previewOpenId && !isValidOpenId(params.previewOpenId)) { errors.push('previewOpenId格式不正确'); } return { valid: errors.length === 0, errors }; }
- Helper function generateCoverImage() that automatically creates attractive cover images from title/content if none provided, supporting PNG via canvas or SVG fallback, with themed designs.static async generateCoverImage(title, content) { try { const path = await import('path'); const fs = await import('fs/promises'); // 提取文章关键信息 const cleanTitle = title.replace(/[#*`]/g, '').trim(); const shortTitle = cleanTitle.length > 20 ? cleanTitle.substring(0, 20) + '...' : cleanTitle; // 从内容中提取关键词或副标题 const contentLines = content.split('\n').filter(line => line.trim()); let subtitle = ''; for (const line of contentLines) { const cleanLine = line.replace(/[#*`<>]/g, '').trim(); if (cleanLine.length > 10 && cleanLine.length < 50 && !cleanLine.includes('http')) { subtitle = cleanLine; break; } } if (!subtitle) { subtitle = '精彩内容,值得一读'; } // 选择背景颜色(根据标题内容智能选择) const colors = [ { bg: '#3498db', text: '#ffffff', accent: '#2980b9' }, // 蓝色主题 { bg: '#e74c3c', text: '#ffffff', accent: '#c0392b' }, // 红色主题 { bg: '#2ecc71', text: '#ffffff', accent: '#27ae60' }, // 绿色主题 { bg: '#9b59b6', text: '#ffffff', accent: '#8e44ad' }, // 紫色主题 { bg: '#f39c12', text: '#ffffff', accent: '#e67e22' }, // 橙色主题 { bg: '#1abc9c', text: '#ffffff', accent: '#16a085' }, // 青色主题 ]; // 根据标题内容选择颜色 let colorIndex = 0; if (title.includes('AI') || title.includes('技术')) colorIndex = 0; else if (title.includes('重要') || title.includes('紧急')) colorIndex = 1; else if (title.includes('成功') || title.includes('增长')) colorIndex = 2; else if (title.includes('创新') || title.includes('未来')) colorIndex = 3; else if (title.includes('警告') || title.includes('注意')) colorIndex = 4; else colorIndex = Math.floor(Math.random() * colors.length); const theme = colors[colorIndex]; // 创建Canvas并生成PNG图片 const timestamp = Date.now(); const coverPath = path.default.join(process.cwd(), `auto-cover-${timestamp}.png`); // 使用Canvas API生成PNG图片 await WeChatPublisher.createPngCover({ title: shortTitle, subtitle: subtitle.substring(0, 30), theme, outputPath: coverPath }); // 检查文件大小 const stats = await fs.stat(coverPath); const fileSizeInMB = stats.size / (1024 * 1024); if (fileSizeInMB > 1) { logger.warn('生成的封面图超过1MB,尝试压缩', { size: `${fileSizeInMB.toFixed(2)}MB` }); // 如果文件过大,可以在这里添加压缩逻辑 } logger.info('自动生成封面图成功', { coverPath, title: shortTitle, size: `${fileSizeInMB.toFixed(2)}MB` }); return coverPath; } catch (error) { logger.error('自动生成封面图失败', { error: error.message }); throw new Error(`自动生成封面图失败: ${error.message}`); } }