import os import tempfile import pypandoc from typing import Optional from fastapi import APIRouter, HTTPException, Depends from fastapi.responses import FileResponse from pydantic import BaseModel from starlette.background import BackgroundTask import docx from docx.shared import Pt from docx.oxml.ns import qn import auth import db from formula_processor import preprocess_formulas router = APIRouter() class StyleOptions(BaseModel): base_font_size: Optional[int] = 12 base_font_family: Optional[str] = "宋体" h1_size: Optional[int] = 18 h2_size: Optional[int] = 16 h3_size: Optional[int] = 14 h4_size: Optional[int] = 12 class DocRequest(BaseModel): markdown: str styleOptions: Optional[StyleOptions] = None def remove_file(path: str): try: os.remove(path) except Exception: pass @router.post("/api/convert") async def convert_md_to_docx(req: DocRequest, current_user: Optional[dict] = Depends(auth.get_optional_user)): try: # 如果带有合法登录 token,则记录历史 if current_user: db.add_history_record(current_user["id"], req.markdown) try: pypandoc.get_pandoc_version() except OSError: pypandoc.download_pandoc() # ====== 公式预处理:统一各种定界符格式 ====== # 将 GPT/AI 生成内容中 \(...\)、\[...\]、缺失反斜杠的 [...] 和 (...) # 全部转换为 Pandoc 可识别的标准 $...$ 和 $$...$$ 格式 processed_markdown = preprocess_formulas(req.markdown) fd, tmp_path = tempfile.mkstemp(suffix=".docx") os.close(fd) md_fd, md_tmp_path = tempfile.mkstemp(suffix=".md") with os.fdopen(md_fd, 'w', encoding='utf-8') as f: f.write(processed_markdown) pypandoc.convert_file( md_tmp_path, 'docx', format='markdown+tex_math_dollars', outputfile=tmp_path ) os.remove(md_tmp_path) # ====== 样式后处理 ====== if req.styleOptions: doc = docx.Document(tmp_path) def set_font(style_name, font_name, font_size): try: style = doc.styles[style_name] if font_size: style.font.size = Pt(font_size) if font_name: style.font.name = font_name style._element.rPr.rFonts.set(qn('w:eastAsia'), font_name) except KeyError: pass set_font('Normal', req.styleOptions.base_font_family, req.styleOptions.base_font_size) set_font('Heading 1', req.styleOptions.base_font_family, req.styleOptions.h1_size) set_font('Heading 2', req.styleOptions.base_font_family, req.styleOptions.h2_size) set_font('Heading 3', req.styleOptions.base_font_family, req.styleOptions.h3_size) set_font('Heading 4', req.styleOptions.base_font_family, req.styleOptions.h4_size) set_font('Heading 5', req.styleOptions.base_font_family, req.styleOptions.h4_size) set_font('Heading 6', req.styleOptions.base_font_family, req.styleOptions.h4_size) # 强制遍历并覆盖所有段落的 run,确保样式生效 for p in doc.paragraphs: # 判定当前段落是不是标题 current_font_size = req.styleOptions.base_font_size current_font_family = req.styleOptions.base_font_family if p.style.name.startswith('Heading'): level_str = p.style.name.replace('Heading ', '') try: level = int(level_str) if level == 1: current_font_size = req.styleOptions.h1_size elif level == 2: current_font_size = req.styleOptions.h2_size elif level == 3: current_font_size = req.styleOptions.h3_size elif level >= 4: current_font_size = req.styleOptions.h4_size except: pass # 为该段落中每一个具体的 run 设置字体 for run in p.runs: run.font.name = current_font_family run._element.rPr.rFonts.set(qn('w:eastAsia'), current_font_family) if current_font_size: run.font.size = Pt(current_font_size) doc.save(tmp_path) return FileResponse( tmp_path, media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document', filename='document.docx', background=BackgroundTask(remove_file, tmp_path) ) except Exception as e: print(f"Error during conversion: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/api/history") async def get_history(current_user: dict = Depends(auth.get_current_user)): history = db.get_history_records(current_user["id"]) return {"history": history}