Varshith dharmaj commited on
Commit
9b28b2a
·
verified ·
1 Parent(s): cc4a250

Upload utils/export_manager.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. utils/export_manager.py +645 -0
utils/export_manager.py ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VibeDoc 多格式导出管理器
3
+ 支持 Ma# PDF 导出
4
+ try:
5
+ from reportlab.lib.pagesizes import letter, A4
6
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
7
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
8
+ from reportlab.lib.units import inch
9
+ from reportlab.lib import colors
10
+ from reportlab.pdfbase import pdfmetrics
11
+ from reportlab.pdfbase.ttfonts import TTFont
12
+ PDF_AVAILABLE = True
13
+ except ImportError:
14
+ PDF_AVAILABLE = False
15
+
16
+ # 高级PDF导出 - 移除weasyprint依赖,使用reportlab
17
+ WEASYPRINT_AVAILABLE = FalseF 格式的文档导出
18
+ """
19
+
20
+ import os
21
+ import io
22
+ import re
23
+ import zipfile
24
+ import tempfile
25
+ from datetime import datetime
26
+ from typing import Dict, Tuple, Optional, Any
27
+ import logging
28
+
29
+ # 核心依赖
30
+ import markdown
31
+ import html2text
32
+
33
+ # Word 导出
34
+ try:
35
+ from docx import Document
36
+ from docx.shared import Inches
37
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
38
+ from docx.enum.style import WD_STYLE_TYPE
39
+ DOCX_AVAILABLE = True
40
+ except ImportError:
41
+ DOCX_AVAILABLE = False
42
+
43
+ # PDF 导出
44
+ try:
45
+ from reportlab.lib.pagesizes import letter, A4
46
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
47
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
48
+ from reportlab.lib.units import inch
49
+ from reportlab.lib import colors
50
+ from reportlab.pdfbase import pdfmetrics
51
+ from reportlab.pdfbase.ttfonts import TTFont
52
+ PDF_AVAILABLE = True
53
+ except ImportError:
54
+ PDF_AVAILABLE = False
55
+
56
+ # 高级PDF导出(备用方案) - 移除weasyprint依赖
57
+ WEASYPRINT_AVAILABLE = False
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+ class ExportManager:
62
+ """多格式导出管理器"""
63
+
64
+ def __init__(self):
65
+ self.supported_formats = ['markdown', 'html']
66
+
67
+ # 检查可选依赖
68
+ if DOCX_AVAILABLE:
69
+ self.supported_formats.append('docx')
70
+ if PDF_AVAILABLE:
71
+ self.supported_formats.append('pdf')
72
+
73
+ logger.info(f"📄 ExportManager 初始化完成,支持格式: {', '.join(self.supported_formats)}")
74
+
75
+ def get_supported_formats(self) -> list:
76
+ """获取支持的导出格式"""
77
+ return self.supported_formats.copy()
78
+
79
+ def export_to_markdown(self, content: str, metadata: Optional[Dict] = None) -> str:
80
+ """
81
+ 导出为 Markdown 格式(清理和优化)
82
+
83
+ Args:
84
+ content: 原始内容
85
+ metadata: 元数据信息
86
+
87
+ Returns:
88
+ str: 优化后的 Markdown 内容
89
+ """
90
+ try:
91
+ # 添加文档头部信息
92
+ if metadata:
93
+ header = f"""---
94
+ title: {metadata.get('title', 'VibeDoc 开发计划')}
95
+ author: {metadata.get('author', 'VibeDoc AI Agent')}
96
+ date: {metadata.get('date', datetime.now().strftime('%Y-%m-%d'))}
97
+ generator: VibeDoc AI Agent v1.0
98
+ ---
99
+
100
+ """
101
+ content = header + content
102
+
103
+ # 清理和优化内容
104
+ content = self._clean_markdown_content(content)
105
+
106
+ logger.info("✅ Markdown 导出成功")
107
+ return content
108
+
109
+ except Exception as e:
110
+ logger.error(f"❌ Markdown 导出失败: {e}")
111
+ return content # 返回原始内容
112
+
113
+ def export_to_html(self, content: str, metadata: Optional[Dict] = None) -> str:
114
+ """
115
+ 导出为 HTML 格式(带样式)
116
+
117
+ Args:
118
+ content: Markdown 内容
119
+ metadata: 元数据信息
120
+
121
+ Returns:
122
+ str: 完整的 HTML 内容
123
+ """
124
+ try:
125
+ # 配置 Markdown 扩展
126
+ md = markdown.Markdown(
127
+ extensions=[
128
+ 'markdown.extensions.extra',
129
+ 'markdown.extensions.codehilite',
130
+ 'markdown.extensions.toc',
131
+ 'markdown.extensions.tables'
132
+ ],
133
+ extension_configs={
134
+ 'codehilite': {
135
+ 'css_class': 'highlight',
136
+ 'use_pygments': False
137
+ },
138
+ 'toc': {
139
+ 'title': '目录'
140
+ }
141
+ }
142
+ )
143
+
144
+ # 转换 Markdown 到 HTML
145
+ html_content = md.convert(content)
146
+
147
+ # 生成完整的 HTML 文档
148
+ title = metadata.get('title', 'VibeDoc 开发计划') if metadata else 'VibeDoc 开发计划'
149
+ author = metadata.get('author', 'VibeDoc AI Agent') if metadata else 'VibeDoc AI Agent'
150
+ date = metadata.get('date', datetime.now().strftime('%Y-%m-%d')) if metadata else datetime.now().strftime('%Y-%m-%d')
151
+
152
+ full_html = f"""<!DOCTYPE html>
153
+ <html lang="zh-CN">
154
+ <head>
155
+ <meta charset="UTF-8">
156
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
157
+ <title>{title}</title>
158
+ <meta name="author" content="{author}">
159
+ <meta name="generator" content="VibeDoc AI Agent">
160
+ <style>
161
+ {self._get_html_styles()}
162
+ </style>
163
+ <!-- Mermaid 支持 -->
164
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
165
+ <script>
166
+ document.addEventListener('DOMContentLoaded', function() {{
167
+ mermaid.initialize({{
168
+ startOnLoad: true,
169
+ theme: 'default',
170
+ securityLevel: 'loose',
171
+ flowchart: {{ useMaxWidth: true }}
172
+ }});
173
+ }});
174
+ </script>
175
+ </head>
176
+ <body>
177
+ <div class="container">
178
+ <header class="document-header">
179
+ <h1>{title}</h1>
180
+ <div class="meta-info">
181
+ <span class="author">📝 {author}</span>
182
+ <span class="date">📅 {date}</span>
183
+ <span class="generator">🤖 Generated by VibeDoc AI Agent</span>
184
+ </div>
185
+ </header>
186
+
187
+ <main class="content">
188
+ {html_content}
189
+ </main>
190
+
191
+ <footer class="document-footer">
192
+ <p>本文档由 <strong>VibeDoc AI Agent</strong> 生成 | 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
193
+ </footer>
194
+ </div>
195
+ </body>
196
+ </html>"""
197
+
198
+ logger.info("✅ HTML 导出成功")
199
+ return full_html
200
+
201
+ except Exception as e:
202
+ logger.error(f"❌ HTML 导出失败: {e}")
203
+ # 简单的 HTML 备用方案
204
+ return f"""<!DOCTYPE html>
205
+ <html><head><title>VibeDoc 开发计划</title></head>
206
+ <body><pre>{content}</pre></body></html>"""
207
+
208
+ def export_to_docx(self, content: str, metadata: Optional[Dict] = None) -> bytes:
209
+ """
210
+ 导出为 Word 文档格式
211
+
212
+ Args:
213
+ content: Markdown 内容
214
+ metadata: 元数据信息
215
+
216
+ Returns:
217
+ bytes: Word 文档的二进制数据
218
+ """
219
+ if not DOCX_AVAILABLE:
220
+ raise ImportError("python-docx 未安装,无法导出 Word 格式")
221
+
222
+ try:
223
+ # 创建新文档
224
+ doc = Document()
225
+
226
+ # 设置文档属性
227
+ properties = doc.core_properties
228
+ properties.title = metadata.get('title', 'VibeDoc 开发计划') if metadata else 'VibeDoc 开发计划'
229
+ properties.author = metadata.get('author', 'VibeDoc AI Agent') if metadata else 'VibeDoc AI Agent'
230
+ properties.subject = 'AI驱动的智能开发计划'
231
+ properties.comments = 'Generated by VibeDoc AI Agent'
232
+
233
+ # 添加标题
234
+ title = doc.add_heading(properties.title, 0)
235
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
236
+
237
+ # 添加元信息
238
+ doc.add_paragraph()
239
+ meta_para = doc.add_paragraph()
240
+ meta_para.add_run(f"📝 作者: {properties.author}").bold = True
241
+ meta_para.add_run("\n")
242
+ meta_para.add_run(f"📅 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}").bold = True
243
+ meta_para.add_run("\n")
244
+ meta_para.add_run("🤖 生成工具: VibeDoc AI Agent").bold = True
245
+
246
+ doc.add_paragraph()
247
+ doc.add_paragraph("─" * 50)
248
+ doc.add_paragraph()
249
+
250
+ # 解析和添加内容
251
+ self._parse_markdown_to_docx(doc, content)
252
+
253
+ # 添加页脚
254
+ doc.add_paragraph()
255
+ doc.add_paragraph("─" * 50)
256
+ footer_para = doc.add_paragraph()
257
+ footer_para.add_run("本文档由 VibeDoc AI Agent 自动生成").italic = True
258
+ footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
259
+
260
+ # 保存到内存
261
+ doc_stream = io.BytesIO()
262
+ doc.save(doc_stream)
263
+ doc_stream.seek(0)
264
+
265
+ logger.info("✅ Word 文档导出成功")
266
+ return doc_stream.getvalue()
267
+
268
+ except Exception as e:
269
+ logger.error(f"❌ Word 导出失败: {e}")
270
+ raise
271
+
272
+ def export_to_pdf(self, content: str, metadata: Optional[Dict] = None) -> bytes:
273
+ """
274
+ 导出为 PDF 格式
275
+
276
+ Args:
277
+ content: Markdown 内容
278
+ metadata: 元数据信息
279
+
280
+ Returns:
281
+ bytes: PDF 文档的二进制数据
282
+ """
283
+ if PDF_AVAILABLE:
284
+ return self._export_pdf_reportlab(content, metadata)
285
+ else:
286
+ raise ImportError("PDF 导出依赖未安装")
287
+
288
+ def create_multi_format_export(self, content: str, formats: list = None, metadata: Optional[Dict] = None) -> bytes:
289
+ """
290
+ 创建多格式导出的 ZIP 包
291
+
292
+ Args:
293
+ content: 原始内容
294
+ formats: 要导出的格式列表,默认为所有支持的格式
295
+ metadata: 元数据信息
296
+
297
+ Returns:
298
+ bytes: ZIP 文件的二进制数据
299
+ """
300
+ if formats is None:
301
+ formats = self.supported_formats
302
+
303
+ # 验证格式
304
+ invalid_formats = set(formats) - set(self.supported_formats)
305
+ if invalid_formats:
306
+ raise ValueError(f"不支持的格式: {', '.join(invalid_formats)}")
307
+
308
+ try:
309
+ # 创建内存中的 ZIP 文件
310
+ zip_buffer = io.BytesIO()
311
+
312
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
313
+ # 生成基础文件名
314
+ base_name = metadata.get('title', 'vibedoc_plan') if metadata else 'vibedoc_plan'
315
+ base_name = re.sub(r'[^\w\-_\.]', '_', base_name) # 清理文件名
316
+
317
+ # 导出各种格式
318
+ for fmt in formats:
319
+ try:
320
+ if fmt == 'markdown':
321
+ file_content = self.export_to_markdown(content, metadata)
322
+ zip_file.writestr(f"{base_name}.md", file_content.encode('utf-8'))
323
+
324
+ elif fmt == 'html':
325
+ file_content = self.export_to_html(content, metadata)
326
+ zip_file.writestr(f"{base_name}.html", file_content.encode('utf-8'))
327
+
328
+ elif fmt == 'docx' and DOCX_AVAILABLE:
329
+ file_content = self.export_to_docx(content, metadata)
330
+ zip_file.writestr(f"{base_name}.docx", file_content)
331
+
332
+ elif fmt == 'pdf' and PDF_AVAILABLE:
333
+ file_content = self.export_to_pdf(content, metadata)
334
+ zip_file.writestr(f"{base_name}.pdf", file_content)
335
+
336
+ except Exception as e:
337
+ logger.warning(f"⚠️ 格式 {fmt} 导出失败: {e}")
338
+ # 在 ZIP 中添加错误信息文件
339
+ error_msg = f"格式 {fmt} 导出失败:\n{str(e)}\n\n请检查相关依赖是否正确安装。"
340
+ zip_file.writestr(f"ERROR_{fmt}.txt", error_msg.encode('utf-8'))
341
+
342
+ # 添加说明文件
343
+ readme_content = f"""# VibeDoc 导出文件包
344
+
345
+ ## 📋 文件说明
346
+ 本压缩包包含了您的开发计划的多种格式导出:
347
+
348
+ ### 📄 支持的格式:
349
+ - **Markdown (.md)**: 原始格式,支持所有 Markdown 语法
350
+ - **HTML (.html)**: 网页格式,包含样式和 Mermaid 图表支持
351
+ - **Word (.docx)**: Microsoft Word 文档格式
352
+ - **PDF (.pdf)**: 便携式文档格式
353
+
354
+ ### 🤖 生成信息:
355
+ - 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
356
+ - 生成工具: VibeDoc AI Agent v1.0
357
+ - 项目地址: https://github.com/JasonRobertDestiny/VibeDocs
358
+
359
+ ### 💡 使用建议:
360
+ 1. 优先使用 HTML 格式查看,支持最佳的视觉效果
361
+ 2. 使用 Markdown 格式进行进一步编辑
362
+ 3. 使用 Word 格式进行正式文档处理
363
+ 4. 使用 PDF 格式进行分享和打印
364
+
365
+ ---
366
+ 感谢使用 VibeDoc AI Agent!
367
+ """
368
+ zip_file.writestr("README.md", readme_content.encode('utf-8'))
369
+
370
+ zip_buffer.seek(0)
371
+ logger.info(f"✅ 多格式导出成功,包含 {len(formats)} 种格式")
372
+ return zip_buffer.getvalue()
373
+
374
+ except Exception as e:
375
+ logger.error(f"❌ 多格式导出失败: {e}")
376
+ raise
377
+
378
+ def _clean_markdown_content(self, content: str) -> str:
379
+ """清理和优化 Markdown 内容"""
380
+ # 修复常见的格式问题
381
+ content = re.sub(r'\n{3,}', '\n\n', content) # 移除多余空行
382
+ content = re.sub(r'(?m)^[ \t]+$', '', content) # 移除只有空格的行
383
+ content = content.strip()
384
+
385
+ return content
386
+
387
+ def _get_html_styles(self) -> str:
388
+ """获取 HTML 样式"""
389
+ return """
390
+ * {
391
+ margin: 0;
392
+ padding: 0;
393
+ box-sizing: border-box;
394
+ }
395
+
396
+ body {
397
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
398
+ line-height: 1.6;
399
+ color: #333;
400
+ background: #f8fafc;
401
+ }
402
+
403
+ .container {
404
+ max-width: 900px;
405
+ margin: 0 auto;
406
+ padding: 20px;
407
+ background: white;
408
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
409
+ border-radius: 8px;
410
+ margin-top: 20px;
411
+ margin-bottom: 20px;
412
+ }
413
+
414
+ .document-header {
415
+ text-align: center;
416
+ border-bottom: 3px solid #667eea;
417
+ padding-bottom: 20px;
418
+ margin-bottom: 30px;
419
+ }
420
+
421
+ .document-header h1 {
422
+ color: #667eea;
423
+ font-size: 2.2em;
424
+ margin-bottom: 15px;
425
+ }
426
+
427
+ .meta-info {
428
+ display: flex;
429
+ justify-content: center;
430
+ gap: 20px;
431
+ flex-wrap: wrap;
432
+ color: #666;
433
+ font-size: 0.9em;
434
+ }
435
+
436
+ .content h1, .content h2, .content h3, .content h4 {
437
+ color: #2d3748;
438
+ margin-top: 2em;
439
+ margin-bottom: 1em;
440
+ }
441
+
442
+ .content h1 { border-bottom: 2px solid #667eea; padding-bottom: 0.5em; }
443
+ .content h2 { border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3em; }
444
+
445
+ .content p {
446
+ margin-bottom: 1em;
447
+ text-align: justify;
448
+ }
449
+
450
+ .content ul, .content ol {
451
+ margin-bottom: 1em;
452
+ padding-left: 2em;
453
+ }
454
+
455
+ .content li {
456
+ margin-bottom: 0.5em;
457
+ }
458
+
459
+ .content pre {
460
+ background: #2d3748;
461
+ color: #e2e8f0;
462
+ padding: 1em;
463
+ border-radius: 6px;
464
+ overflow-x: auto;
465
+ margin: 1em 0;
466
+ }
467
+
468
+ .content code {
469
+ background: #f7fafc;
470
+ padding: 0.2em 0.4em;
471
+ border-radius: 3px;
472
+ font-family: 'SFMono-Regular', Consolas, monospace;
473
+ }
474
+
475
+ .content table {
476
+ width: 100%;
477
+ border-collapse: collapse;
478
+ margin: 1em 0;
479
+ }
480
+
481
+ .content th, .content td {
482
+ border: 1px solid #e2e8f0;
483
+ padding: 0.75em;
484
+ text-align: left;
485
+ }
486
+
487
+ .content th {
488
+ background: #f7fafc;
489
+ font-weight: 600;
490
+ }
491
+
492
+ .content blockquote {
493
+ border-left: 4px solid #667eea;
494
+ margin: 1em 0;
495
+ padding-left: 1em;
496
+ color: #666;
497
+ font-style: italic;
498
+ }
499
+
500
+ .mermaid {
501
+ text-align: center;
502
+ margin: 2em 0;
503
+ }
504
+
505
+ .document-footer {
506
+ margin-top: 3em;
507
+ padding-top: 20px;
508
+ border-top: 1px solid #e2e8f0;
509
+ text-align: center;
510
+ color: #666;
511
+ font-size: 0.9em;
512
+ }
513
+
514
+ @media (max-width: 768px) {
515
+ .container {
516
+ margin: 10px;
517
+ padding: 15px;
518
+ }
519
+
520
+ .meta-info {
521
+ flex-direction: column;
522
+ gap: 10px;
523
+ }
524
+ }
525
+ """
526
+
527
+ def _parse_markdown_to_docx(self, doc: "Document", content: str):
528
+ """解析 Markdown 内容并添加到 Word 文档"""
529
+ lines = content.split('\n')
530
+
531
+ for line in lines:
532
+ line = line.strip()
533
+
534
+ if not line:
535
+ continue
536
+
537
+ # 标题处理
538
+ if line.startswith('#'):
539
+ level = len(line) - len(line.lstrip('#'))
540
+ title_text = line.lstrip('#').strip()
541
+ if level <= 6:
542
+ doc.add_heading(title_text, level)
543
+ continue
544
+
545
+ # 代码块处理(简化)
546
+ if line.startswith('```'):
547
+ continue
548
+
549
+ # 列表处理
550
+ if line.startswith('- ') or line.startswith('* '):
551
+ text = line[2:].strip()
552
+ para = doc.add_paragraph(text, style='List Bullet')
553
+ continue
554
+
555
+ if re.match(r'^\d+\.', line):
556
+ text = re.sub(r'^\d+\.\s*', '', line)
557
+ para = doc.add_paragraph(text, style='List Number')
558
+ continue
559
+
560
+ # 普通段落
561
+ if line:
562
+ # 简单的粗体和斜体处理
563
+ line = re.sub(r'\*\*(.*?)\*\*', r'\1', line) # 移除粗体标记,Word 中后续可以手动设置
564
+ line = re.sub(r'\*(.*?)\*', r'\1', line) # 移除斜体标记
565
+ doc.add_paragraph(line)
566
+
567
+ def _export_pdf_reportlab(self, content: str, metadata: Optional[Dict] = None) -> bytes:
568
+ """使用 ReportLab 导出 PDF"""
569
+ try:
570
+ buffer = io.BytesIO()
571
+
572
+ # 创建 PDF 文档
573
+ doc = SimpleDocTemplate(
574
+ buffer,
575
+ pagesize=A4,
576
+ topMargin=1*inch,
577
+ bottomMargin=1*inch,
578
+ leftMargin=1*inch,
579
+ rightMargin=1*inch
580
+ )
581
+
582
+ # 样式设置
583
+ styles = getSampleStyleSheet()
584
+ title_style = ParagraphStyle(
585
+ 'CustomTitle',
586
+ parent=styles['Title'],
587
+ fontSize=20,
588
+ spaceAfter=30,
589
+ alignment=1 # 居中
590
+ )
591
+
592
+ # 构建内容
593
+ story = []
594
+
595
+ # 添加标题
596
+ title = metadata.get('title', 'VibeDoc 开发计划') if metadata else 'VibeDoc 开发计划'
597
+ story.append(Paragraph(title, title_style))
598
+ story.append(Spacer(1, 20))
599
+
600
+ # 添加元信息
601
+ meta_text = f"""
602
+ 作者: {metadata.get('author', 'VibeDoc AI Agent') if metadata else 'VibeDoc AI Agent'}<br/>
603
+ 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}<br/>
604
+ 生成工具: VibeDoc AI Agent
605
+ """
606
+ story.append(Paragraph(meta_text, styles['Normal']))
607
+ story.append(Spacer(1, 30))
608
+
609
+ # 简单处理 Markdown 内容(基础版本)
610
+ lines = content.split('\n')
611
+ for line in lines:
612
+ line = line.strip()
613
+ if not line:
614
+ story.append(Spacer(1, 12))
615
+ continue
616
+
617
+ if line.startswith('#'):
618
+ # 标题
619
+ level = len(line) - len(line.lstrip('#'))
620
+ title_text = line.lstrip('#').strip()
621
+ if level == 1:
622
+ story.append(Paragraph(title_text, styles['Heading1']))
623
+ elif level == 2:
624
+ story.append(Paragraph(title_text, styles['Heading2']))
625
+ else:
626
+ story.append(Paragraph(title_text, styles['Heading3']))
627
+ else:
628
+ # 普通段落
629
+ story.append(Paragraph(line, styles['Normal']))
630
+
631
+ story.append(Spacer(1, 6))
632
+
633
+ # 生成 PDF
634
+ doc.build(story)
635
+ buffer.seek(0)
636
+
637
+ logger.info("✅ PDF 导出成功(ReportLab)")
638
+ return buffer.getvalue()
639
+
640
+ except Exception as e:
641
+ logger.error(f"❌ ReportLab PDF 导出失败: {e}")
642
+ raise
643
+
644
+ # 全局导出管理器实例
645
+ export_manager = ExportManager()