import ConfigControl from '../config/configControl.js'; import puppeteer from 'puppeteer'; import fs from 'fs'; import path from 'path'; import markdownit from 'markdown-it'; import hljs from 'highlight.js'; class Renderer { constructor() { this.browser = null; this.config = null; this.isInitialized = false; } async init() { try { this.config = await ConfigControl.get('ai'); this.browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); this.isInitialized = true; } catch (error) { logger.error(`[crystelf-renderer] 初始化失败: ${error.message}`); } } async renderCode(code, language) { if (!this.isInitialized) await this.init(); try { const page = await this.browser.newPage(); const html = this.getCodeTemplate(code, language, this.config?.codeRenderer || {}); await page.setContent(html, { waitUntil: 'networkidle0' }); await page.waitForSelector('#render-complete', { timeout: 5000 }); const rect = await page.evaluate(() => { const body = document.body; return { width: body.scrollWidth, height: body.scrollHeight }; }); await page.setViewport({ width: Math.ceil(rect.width), height: Math.ceil(rect.height), }); const tempDir = path.join(process.cwd(), 'temp', 'html'); if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true }); const filepath = path.join(tempDir, `code_${Date.now()}.png`); await page.screenshot({ path: filepath, fullPage: false }); await page.close(); logger.info(`[crystelf-ai] 代码渲染完成: ${filepath}`); return filepath; } catch (error) { logger.error(`[crystelf-ai] 代码渲染失败: ${error.message}`); return null; } } async renderMarkdown(markdown) { if (!this.isInitialized) await this.init(); try { const page = await this.browser.newPage(); const html = this.getMarkdownTemplate(markdown, this.config?.markdownRenderer || {}); await page.setContent(html, { waitUntil: 'networkidle0' }); await page.waitForSelector('#render-complete', { timeout: 5000 }); //计算页面尺寸 const rect = await page.evaluate(() => { const body = document.body; const main = document.querySelector('.markdown-body') || body; const rect = main.getBoundingClientRect(); return { width: Math.min(Math.ceil(rect.width + 40), 1200), height: Math.ceil(rect.height + 40), }; }); await page.setViewport({ width: rect.width, height: Math.min(rect.height, 3000), deviceScaleFactor: 2, }); const tempDir = path.join(process.cwd(), 'temp', 'html'); if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true }); const filepath = path.join(tempDir, `markdown_${Date.now()}.png`); await page.screenshot({ path: filepath, fullPage: false }); await page.close(); logger.info(`[crystelf-ai] Markdown渲染完成: ${filepath}`); return filepath; } catch (error) { logger.error(`[crystelf-ai] Markdown渲染失败: ${error.message}`); return null; } } getCodeTemplate(code, language, config = {}) { const themeColor = '#274179'; const fontSize = config.fontSize || 16; const escapedCode = this.escapeHtml(code); const colorMap = { javascript: 'from-yellow-400 to-yellow-600', typescript: 'from-blue-400 to-blue-600', python: 'from-cyan-400 to-cyan-600', html: 'from-orange-400 to-red-500', css: 'from-indigo-400 to-indigo-600', json: 'from-emerald-400 to-emerald-600', yaml: 'from-amber-400 to-amber-600', c: 'from-blue-300 to-blue-500', cpp: 'from-blue-400 to-indigo-600', java: 'from-red-400 to-orange-500', kotlin: 'from-pink-400 to-purple-500', csharp: 'from-violet-400 to-purple-600', 'c#': 'from-violet-400 to-purple-600', dotnet: 'from-purple-400 to-indigo-600', bash: 'from-gray-400 to-gray-600', shell: 'from-gray-400 to-gray-600', text: 'from-slate-400 to-slate-600', }; const barColor = colorMap[language.toLowerCase()] || 'from-cyan-400 to-cyan-600'; let highlightedCode = ''; try { if (hljs.getLanguage(language)) { highlightedCode = hljs.highlight(code, { language, ignoreIllegals: true }).value; } else { highlightedCode = hljs.highlightAuto(code).value; } } catch { highlightedCode = this.escapeHtml(code); } const lines = highlightedCode .split('\n') .map( (line, i) => `
${lines}
' +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
''
);
} catch (__) {}
}
return '' + md.utils.escapeHtml(str) + '';
},
});
return `