UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

196 lines (164 loc) 5.61 kB
import { marked } from 'marked'; import PDFDocument from 'pdfkit'; import { z } from 'zod'; import { DrizzleMigrationModel } from '@/database/models/drizzleMigration'; import { MessageModel } from '@/database/models/message'; import { SessionModel } from '@/database/models/session'; import { DataExporterRepos } from '@/database/repositories/dataExporter'; import { authedProcedure, router } from '@/libs/trpc/lambda'; import { serverDatabase } from '@/libs/trpc/lambda/middleware'; import { ExportDatabaseData } from '@/types/export'; const exportProcedure = authedProcedure.use(serverDatabase).use(async (opts) => { const { ctx } = opts; const dataExporterRepos = new DataExporterRepos(ctx.serverDB, ctx.userId); const drizzleMigration = new DrizzleMigrationModel(ctx.serverDB); const messageModel = new MessageModel(ctx.serverDB, ctx.userId); const sessionModel = new SessionModel(ctx.serverDB, ctx.userId); return opts.next({ ctx: { dataExporterRepos, drizzleMigration, messageModel, sessionModel }, }); }); const REGULAR_FONT_URL = 'https://cdn.jsdelivr.net/gh/adobe-fonts/source-han-sans@2.004R/OTF/SimplifiedChinese/SourceHanSansSC-Regular.otf'; let regularFontCache: Buffer | null = null; const loadRegularFont = async (): Promise<Buffer> => { if (regularFontCache) return regularFontCache; const response = await fetch(REGULAR_FONT_URL); if (!response.ok) { throw new Error(`Failed to fetch font from CDN: ${response.status} ${response.statusText}`); } const fontBuffer = Buffer.from(await response.arrayBuffer()); regularFontCache = fontBuffer; return fontBuffer; }; const generatePdfFromMarkdown = async ( markdownContent: string, title?: string, ): Promise<Buffer> => { const regularFont = await loadRegularFont(); return new Promise((resolve, reject) => { try { const tokens = marked.lexer(markdownContent); const doc = new PDFDocument({ bufferPages: true, margins: { bottom: 50, left: 50, right: 50, top: 50, }, size: 'A4', }); const chunks: Buffer[] = []; doc.registerFont('Regular', regularFont); doc.font('Regular'); doc.on('data', (chunk: Buffer) => chunks.push(chunk)); doc.on('end', () => { const pdfBuffer = Buffer.concat(chunks); resolve(pdfBuffer); }); doc.on('error', reject); if (title) { doc.fontSize(20).text(title, { align: 'center' }); } doc.moveDown(2); let currentY = doc.y; for (const token of tokens) { if (currentY > 700) { doc.addPage(); currentY = 50; } switch (token.type) { case 'heading': { const headingSize = Math.max(16 - (token.depth - 1) * 2, 12); doc.fontSize(headingSize).fillColor('#222').text(token.text, { continued: false }); doc.moveDown(0.5); break; } case 'paragraph': { doc.fontSize(12).fillColor('#333').text(token.text, { align: 'left', lineGap: 2 }); doc.moveDown(1); break; } case 'list': { for (const item of token.items) { doc.fontSize(12).fillColor('#333').text(`• ${item.text}`, { indent: 20, lineGap: 2 }); } doc.moveDown(1); break; } case 'blockquote': { doc.fontSize(12).fillColor('#666').text(token.text, { indent: 20, lineGap: 2 }); doc.moveDown(1); break; } case 'code': { doc.fontSize(10).fillColor('#333').text(token.text, { continued: false, indent: 20, lineGap: 1, }); doc.moveDown(1); break; } case 'hr': { doc.moveTo(50, doc.y).lineTo(545, doc.y).stroke(); doc.moveDown(1); break; } default: { if ('text' in token && token.text) { doc.fontSize(12).fillColor('#333').text(token.text, { align: 'left', lineGap: 2 }); doc.moveDown(1); } break; } } currentY = doc.y; } const pages = doc.bufferedPageRange(); for (let i = 0; i < pages.count; i++) { doc.switchToPage(i); doc .fontSize(8) .fillColor('#666') .text(`Page ${i + 1} of ${pages.count}`, 50, 750, { align: 'center', width: 495, }); } // 完成文档 doc.end(); } catch (error) { reject( new Error( `PDFKit PDF generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, ), ); } }); }; export const exporterRouter = router({ exportData: exportProcedure.mutation(async ({ ctx }): Promise<ExportDatabaseData> => { const data = await ctx.dataExporterRepos.export(5); const schemaHash = await ctx.drizzleMigration.getLatestMigrationHash(); return { data, schemaHash }; }), exportPdf: exportProcedure .input( z.object({ content: z.string(), sessionId: z.string(), title: z.string().optional(), topicId: z.string().optional(), }), ) .mutation(async ({ input }) => { const { content, title } = input; const pdfBuffer = await generatePdfFromMarkdown(content, title); return { filename: `${title}.pdf`, pdf: pdfBuffer.toString('base64'), }; }), });