Files
jerryandClaude Fable 5 5db3ead73e feat(docgen): doc_writer 能力全链——vstore 独立服务 + BFF docgen 路由 + 前端文档工作台
- 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>
2026-09-03 21:13:37 +08:00

91 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")