"""services/docgen/editor.py —— 文档编辑(P1,dev1 归属)。 - apply_edit(md, instruction) -> str: 当前文档全文 + 指令 → 只输出完整新 md;长文档(> CHUNK_EDIT_CHARS)分段改 - smart_export(md, format, template_style) -> Path: 按格式导出 docx/pdf(style 对齐:docx 用 style_hints 覆盖字体) """ from __future__ import annotations import logging from pathlib import Path from typing import Any from . import llm from .settings import CHUNK_EDIT_CHARS logger = logging.getLogger(__name__) EDIT_SYSTEM = ( "你是文档编辑助手。根据用户的修改指令编辑文档,只输出编辑后的完整 " "markdown 文档(保持原文档的标题、列表、表格结构),不要输出任何解释。" ) async def apply_edit(markdown: str, instruction: str) -> dict[str, Any]: """编辑文档:当前文档全文 + 指令 → {markdown, md_path}。 契约(dev2 桩):apply_edit(markdown, instruction) -> dict, router 取 result["markdown"](md_path 可省略)。 长文档(> CHUNK_EDIT_CHARS)分段改:每段独立请求,段间标题不重复。 """ if not instruction or not instruction.strip(): return {"markdown": markdown} user = f"修改指令:{instruction}\n\n当前文档(markdown):\n{markdown}" if len(markdown) <= CHUNK_EDIT_CHARS: edited = await _edit_once(markdown, instruction) return {"markdown": edited} # 长文档分段:按空行切块,每块 ≤ CHUNK_EDIT_CHARS,逐块带指令修改 edited_parts: list[str] = [] for chunk in _split_chunks(markdown, CHUNK_EDIT_CHARS): try: edited = await _edit_once(chunk, instruction) except llm.LLMError as exc: logger.warning("docgen: 分段编辑失败,保留原段:%s", exc) edited = chunk edited_parts.append(edited) return {"markdown": "\n\n".join(edited_parts)} async def _edit_once(markdown: str, instruction: str) -> str: """单次编辑调用(失败抛 LLMError,由调用方决定占位策略)。""" user = f"修改指令:{instruction}\n\n当前文档(markdown):\n{markdown}" return await llm.complete_text(EDIT_SYSTEM, user) def _split_chunks(markdown: str, max_chars: int) -> list[str]: """按空行切块,每块 ≤ max_chars(尽量)。""" paragraphs = markdown.split("\n\n") chunks: list[str] = [] current: list[str] = [] size = 0 for para in paragraphs: if current and size + len(para) + 2 > max_chars: chunks.append("\n\n".join(current)) current = [para] size = len(para) else: current.append(para) size += len(para) + 2 if current: chunks.append("\n\n".join(current)) return chunks def smart_export(md: str, format: str, template_style: dict[str, Any] | None = None) -> Path: """按格式导出:docx/pdf(style 对齐:docx 用 style_hints 覆盖字体)。 返回导出文件路径。转换走 md2docx/md2pdf(纯 CPU 转换,同步执行)。 """ from . import md2docx, md2pdf from .settings import artifacts_dir out_dir = artifacts_dir() out_dir.mkdir(parents=True, exist_ok=True) fmt = (format or "docx").lower() if fmt == "docx": return md2docx.docx_from_markdown(md, out_dir / "export.docx") return md2pdf.pdf_from_markdown(md, out_dir / "export.pdf")