From 88d9e6ddf997867c3aeafb917dcfd202ca743eb2 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Sun, 19 Oct 2025 00:45:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E6=88=AA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ai/renderer.js | 345 +++++++++++++++++++++++---------------------- package.json | 2 + 2 files changed, 176 insertions(+), 171 deletions(-) diff --git a/lib/ai/renderer.js b/lib/ai/renderer.js index 5fa6350..a30beef 100644 --- a/lib/ai/renderer.js +++ b/lib/ai/renderer.js @@ -1,9 +1,10 @@ -import ConfigControl from "../config/configControl.js"; +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; @@ -16,42 +17,36 @@ class Renderer { this.config = await ConfigControl.get('ai'); this.browser = await puppeteer.launch({ headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'] + args: ['--no-sandbox', '--disable-setuid-sandbox'], }); this.isInitialized = true; } catch (error) { - logger.error(`[crystelf-renderer] 初始化失败: ${error.message}`); + console.error(`[crystelf-renderer] 初始化失败: ${error.message}`); } } - /** - * 渲染代码为图片 - * @param code 代码 - * @param language 语言 - * @returns {Promise} - */ async renderCode(code, language = 'text') { - if (!this.isInitialized) { - await this.init(); - } + if (!this.isInitialized) await this.init(); try { const page = await this.browser.newPage(); - const codeConfig = this.config?.codeRenderer || {}; - const html = this.generateCodeHTML(code, language, codeConfig); - await page.setContent(html); - await page.setViewport({ width: 800, height: 600 }); - const tempDir = path.join(process.cwd(), 'temp', 'html'); - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }); - } - const filename = `code_${Date.now()}.png`; - const filepath = path.join(tempDir, filename); - await page.screenshot({ - path: filepath, - fullPage: true, - type: 'png' + 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; @@ -61,32 +56,30 @@ class Renderer { } } - /** - * 渲染md为图片 - * @param markdown - * @returns {Promise} - */ async renderMarkdown(markdown) { - if (!this.isInitialized) { - await this.init(); - } + if (!this.isInitialized) await this.init(); + try { const page = await this.browser.newPage(); - const markdownConfig = this.config?.markdownRenderer || {}; - const html = this.generateMarkdownHTML(markdown, markdownConfig); - await page.setContent(html); - await page.setViewport({ width: 800, height: 600 }); - const tempDir = path.join(process.cwd(), 'temp', 'html'); - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }); - } - const filename = `markdown_${Date.now()}.png`; - const filepath = path.join(tempDir, filename); - await page.screenshot({ - path: filepath, - fullPage: true, - type: 'png' + 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; @@ -96,143 +89,153 @@ class Renderer { } } + getCodeTemplate(code, language = 'text', config = {}) { + const themeColor = '#0f172a'; + const fontSize = config.fontSize || 16; + const escapedCode = this.escapeHtml(code); - /** - * 生成代码html - * @param code 代码内容 - * @param language 语言 - * @param config 配置 - * @returns {string} - */ - generateCodeHTML(code, language, config) { - const theme = config.theme || 'github'; - const fontSize = config.fontSize || 14; - const lineNumbers = config.lineNumbers !== false; - const backgroundColor = config.backgroundColor || '#f6f8fa'; + 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 ` - - - - - Code Render - - - - -
${this.escapeHtml(code)}
- - - -`; + } + + + +
+
+ ${language} +
+
+
${lines}
+
+
+
+ + + `; } - /** - * 生成Markdown HTML - * @param {string} markdown Markdown内容 - * @param {Object} config 配置 - * @returns {string} HTML内容 - */ - generateMarkdownHTML(markdown, config) { - const theme = config.theme || 'light'; - const fontSize = config.fontSize || 14; - const codeTheme = config.codeTheme || 'github'; + 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 ` - - - - - Markdown Render - - - - - -
-
-
- - - - - -`; + + + + + + + + + ${md.render(markdown)} +
+ + + `; } escapeHtml(text) { - const map = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; + const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, (m) => map[m]); } diff --git a/package.json b/package.json index 614bccf..e7ddcb0 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "axios": "^1.8.4", "chalk": "^5.4.1", "form-data": "^4.0.2", + "highlight.js": "^11.11.1", + "markdown-it": "^14.1.0", "openai": "^4.89.0", "pinyin-pro": "^3.27.0", "rss-parser": "^3.13.0"