Skip to main content
Glama

Mermaid Chart MCP

renderer.ts11.8 kB
import puppeteer, { Browser, Page } from 'puppeteer'; import fs from 'fs-extra'; import { dirname } from 'path'; import sharp from 'sharp'; import { MinIOUploader, UploadResult, createDefaultMinIOConfig } from './minio-uploader.js'; export interface RenderOptions { mermaidCode: string; outputPath: string; format?: 'png' | 'svg' | 'pdf'; width?: number; height?: number; backgroundColor?: string; theme?: 'default' | 'dark' | 'forest' | 'neutral'; uploadToMinio?: boolean; // 是否上传到MinIO minioExpiryDays?: number; // MinIO文件有效期(天数),默认7天,最大30天 } export interface RenderResult { outputPath: string; format: string; width: number; height: number; minioUrl?: string; // 新增:MinIO访问链接 uploadResult?: UploadResult; // 新增:上传结果详情 } /** * Mermaid 图表渲染器 * 使用 Puppeteer 和无头浏览器渲染 Mermaid 图表 */ export class MermaidRenderer { private browser: Browser | null = null; constructor() {} /** * 初始化浏览器实例 */ private async initBrowser(): Promise<Browser> { if (!this.browser) { this.browser = await puppeteer.launch({ headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--force-device-scale-factor=2', // 高DPI渲染 '--high-dpi-support', ], }); } return this.browser; } /** * 渲染 Mermaid 图表 */ async renderChart(options: RenderOptions): Promise<RenderResult> { const { mermaidCode, outputPath, format = 'png', width = 1200, height = 800, backgroundColor = 'white', theme = 'default', uploadToMinio = false, minioExpiryDays = 7, } = options; try { // 确保输出目录存在 const outputDir = dirname(outputPath); await fs.ensureDir(outputDir); // 初始化浏览器 const browser = await this.initBrowser(); const page = await browser.newPage(); // 设置高分辨率视口和设备像素比 await page.setViewport({ width, height, deviceScaleFactor: 2, // 设置设备像素比为2,提高PNG渲染质量 }); // 增加页面错误监听 page.on('console', msg => { if (msg.type() === 'error') { console.error('页面控制台错误:', msg.text()); } }); page.on('pageerror', error => { console.error('页面运行时错误:', error); }); // 创建 HTML 内容 const htmlContent = this.createHtmlContent(mermaidCode, theme, backgroundColor); // 加载 HTML 内容 console.log('加载HTML内容...'); await page.setContent(htmlContent, { waitUntil: 'domcontentloaded', timeout: 30000 }); // 等待 Mermaid 渲染完成,增加更长的超时时间 console.log('等待Mermaid渲染...'); try { await page.waitForSelector('#mermaid-diagram', { timeout: 30000 }); // 检查是否渲染成功 const isRendered = await page.waitForFunction( () => { const element = document.querySelector('#mermaid-diagram'); const hasRendered = element && element.getAttribute('data-rendered') === 'true'; const hasSvg = element && element.querySelector('svg'); console.log('渲染状态检查:', { hasRendered, hasSvg }); return hasRendered || hasSvg; // 任一条件满足即可 }, { timeout: 60000, polling: 1000 } ); console.log('Mermaid渲染完成'); } catch (timeoutError) { // 如果超时,尝试获取页面状态信息 const pageContent = await page.content(); console.error('渲染超时,页面内容:', pageContent.substring(0, 500)); const diagramElement = await page.$('#mermaid-diagram'); if (diagramElement) { const innerHTML = await page.evaluate(el => el.innerHTML, diagramElement); console.error('图表元素内容:', innerHTML); } throw new Error(`Mermaid渲染超时: ${timeoutError instanceof Error ? timeoutError.message : String(timeoutError)}`); } // 根据格式渲染 if (format === 'svg') { await this.renderSVG(page, outputPath); } else if (format === 'pdf') { await this.renderPDF(page, outputPath, width, height); } else { await this.renderPNG(page, outputPath, width, height); } await page.close(); // 基础渲染结果 const result: RenderResult = { outputPath, format, width, height, }; // 如果需要上传到MinIO if (uploadToMinio) { try { console.log('🔄 开始MinIO上传...'); const minioConfig = createDefaultMinIOConfig(); const minioUploader = new MinIOUploader(minioConfig); await minioUploader.initialize(); const uploadResult = await minioUploader.uploadFile(outputPath, { expiryDays: minioExpiryDays }); result.uploadResult = uploadResult; if (uploadResult.success && uploadResult.url) { result.minioUrl = uploadResult.url; console.log('✅ MinIO上传成功'); } else { console.error('❌ MinIO上传失败:', uploadResult.error); } } catch (minioError) { console.error('❌ MinIO上传过程中发生错误:', minioError); result.uploadResult = { success: false, error: `MinIO上传失败: ${minioError instanceof Error ? minioError.message : String(minioError)}` }; } } return result; } catch (error) { throw new Error(`渲染失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 创建包含 Mermaid 图表的 HTML 内容 */ private createHtmlContent(mermaidCode: string, theme: string, backgroundColor: string): string { return `<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mermaid Chart</title> <script src="https://unpkg.com/mermaid@10.6.1/dist/mermaid.min.js"></script> <style> body { margin: 0; padding: 20px; background-color: ${backgroundColor}; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: 'Microsoft YaHei', 'SimHei', 'Arial', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } #mermaid-diagram { max-width: 100%; max-height: 100%; } .mermaid { background-color: ${backgroundColor}; } svg text { font-family: 'Microsoft YaHei', 'SimHei', 'Arial', sans-serif !important; font-size: 14px !important; font-weight: 500 !important; } svg .node rect, svg .node circle, svg .node ellipse, svg .node polygon { stroke-width: 2px !important; } #loading { display: none; } </style> </head> <body> <div id="loading">Loading Mermaid...</div> <div id="mermaid-diagram" class="mermaid"> ${mermaidCode} </div> <script> console.log('Mermaid script loaded'); mermaid.initialize({ startOnLoad: false, theme: '${theme}', securityLevel: 'loose', flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis', padding: 15 }, sequence: { useMaxWidth: true, diagramMarginX: 50, diagramMarginY: 10, actorMargin: 50, width: 150, height: 65, boxMargin: 10, boxTextMargin: 5, noteMargin: 10, messageMargin: 35 }, gantt: { useMaxWidth: true, fontSize: 11, fontFamily: 'Microsoft YaHei' }, fontFamily: 'Microsoft YaHei, SimHei, Arial, sans-serif', fontSize: 14 }); async function renderDiagram() { try { console.log('Starting mermaid render'); const element = document.getElementById('mermaid-diagram'); await mermaid.run({ nodes: [element] }); console.log('Mermaid render completed'); if (element) { element.setAttribute('data-rendered', 'true'); } } catch (error) { console.error('Mermaid render error:', error); const errorMsg = error && error.message ? error.message : 'Unknown error'; document.body.innerHTML = '<div style="color: red;">Render Error: ' + errorMsg + '</div>'; } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', renderDiagram); } else { renderDiagram(); } </script> </body> </html>`; } /** * 渲染为 PNG 格式 */ private async renderPNG(page: Page, outputPath: string, width: number, height: number): Promise<void> { // 获取 SVG 元素 const svgElement = await page.$('#mermaid-diagram svg'); if (!svgElement) { throw new Error('无法找到生成的 SVG 元素'); } // 截取 SVG 元素的截图,使用高质量设置 const screenshot = await svgElement.screenshot({ type: 'png', omitBackground: false, clip: undefined, // 让Puppeteer自动检测边界 }); // 使用 Sharp 处理图像,提高质量 const processedImage = await sharp(screenshot) .resize(width * 2, height * 2, { // 先放大2倍提高质量 fit: 'inside', withoutEnlargement: false, background: 'white', }) .resize(width, height, { // 再缩小到目标尺寸,实现抗锯齿效果 fit: 'inside', withoutEnlargement: false, kernel: sharp.kernel.lanczos3, // 使用高质量缩放算法 }) .png({ quality: 100, compressionLevel: 0, // 不压缩,保持最高质量 adaptiveFiltering: false, force: true }) .toBuffer(); await fs.writeFile(outputPath, processedImage); } /** * 渲染为 SVG 格式 */ private async renderSVG(page: Page, outputPath: string): Promise<void> { const svgContent = await page.$eval('#mermaid-diagram svg', (element: Element) => { return element.outerHTML; }); if (!svgContent) { throw new Error('无法获取 SVG 内容'); } await fs.writeFile(outputPath, svgContent, 'utf-8'); } /** * 渲染为 PDF 格式 */ private async renderPDF(page: Page, outputPath: string, width: number, height: number): Promise<void> { const pdf = await page.pdf({ path: outputPath, format: 'A4', printBackground: true, width: `${width}px`, height: `${height}px`, margin: { top: '20px', right: '20px', bottom: '20px', left: '20px', }, }); } /** * 清理资源 */ async cleanup(): Promise<void> { if (this.browser) { await this.browser.close(); this.browser = null; } } }

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/pickstar-2002/mermaid-chart-mcp'

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