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();