- vstore 独立安装包(8094):解析/嵌入/加密 vault/faiss 检索,存储路径支持本地 file 与 http(s):// URL,启动握手校验,集群化预留(embed 独立模块) - BFF docgen 路由:上传入库/generate/编辑/导出/下载/主库管理,意图护栏(与前端同词典),vstore 失败兜底,quota 507 与 TTL 惰性清理 - 前端:自然触发意图路由、拖拽上传 chips、能力徽标、右侧文档 TAB(WYSIWYG 编辑/prompt 修改/PDF+DOCX 导出下载)、右栏拖宽+手柄仿左栏 - 批量导入工具 docgen_import.py(并发 8、导入即加密+删源明文) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""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")
|