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) { console.error(`[crystelf-renderer] 初始化失败: ${error.message}`); } } async renderCode(code, language = 'text') { 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; 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, `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 = 'text', config = {}) { const themeColor = '#0f172a'; 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'; const highlightedCode = hljs.highlight(code, { language }).value; const lines = highlightedCode .split('\n') .map( (line, i) => `
${i + 1} ${line}
` ) .join(''); return `
${language}
${lines}
`; } getMarkdownTemplate(markdown, config = {}) { const themeColor = '#0f172a'; const fontSize = config.fontSize || 18; const md = markdownit({ html: true, linkify: true, typographer: true, highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return ( '
' +
              hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
              '
' ); } catch (__) {} } return '
' + md.utils.escapeHtml(str) + '
'; }, }); return ` ${md.render(markdown)}
`; } escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, (m) => map[m]); } async close() { if (this.browser) { await this.browser.close(); this.browser = null; this.isInitialized = false; } } } export default new Renderer();