feat(compose): 标书docx合成支持章节markdown图片行![alt](url)——下载invoke_model产物真图add_picture嵌入(居中+图注),下载失败保留文字占位诚实降级(2026-09-07用户需求:章节配图用文生图真图非字符画)

This commit is contained in:
yumoqing 2026-09-07 17:31:28 +08:00
parent ff20d6fa14
commit 3f6790edad

View File

@ -35,11 +35,57 @@ async def _workspace_dir(sor, project_id):
return ''
def _fetch_image_bytes(url, timeout=30):
"""下载配图(章节正文里的 markdown 图片 URL → 本地字节流)。
URL invoke_model 产物经 downloadfile2url 落地的本地持久地址
/idfile 静态路径上游 24h 时效 URL 已在推理层落地这里拿到的是
平台自身地址http(s) 下载本地绝对路径直读失败返回 None诚实
降级docx 里保留文字说明不假装成功
"""
try:
u = (url or '').strip()
if not u:
return None
if u.startswith('http://') or u.startswith('https://'):
import urllib.request
req = urllib.request.Request(u, headers={'User-Agent': 'pipeline-bidding'})
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = resp.read()
return data if data else None
if u.startswith('/') and os.path.isfile(u):
with open(u, 'rb') as f:
return f.read()
except Exception as e:
logger.warning("compose: 配图下载失败 url=%s: %s", (url or '')[:120], e)
return None
# markdown 图片行:![alt](url)(章节写作者按角色 prompt 规范嵌入 invoke_model 产物)
_MD_IMG_RE = None
def _parse_md_image(line):
"""解析一行 markdown 图片语法,返回 (alt, url) 或 None。"""
global _MD_IMG_RE
if _MD_IMG_RE is None:
import re
_MD_IMG_RE = re.compile(r'^\s*!\[([^\]]*)\]\(([^)\s]+)\)\s*$')
m = _MD_IMG_RE.match(line or '')
if not m:
return None
return m.group(1), m.group(2)
def _write_docx(path, title, subtitle, chapters):
"""用 python-docx 写标书。返回 (ok, page_hint_or_err)。"""
"""用 python-docx 写标书。返回 (ok, page_hint_or_err)。
章节正文支持 markdown 图片行 ![alt](url)写作者经 invoke_model 生成的
真实配图下载后 add_picture 嵌入下载失败保留文字占位诚实降级
"""
try:
from docx import Document
from docx.shared import Pt
from docx.shared import Pt, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError:
return False, "python-docx 未安装"
@ -72,12 +118,35 @@ def _write_docx(path, title, subtitle, chapters):
doc.add_paragraph("%s %s" % (c.get("chapter_no", ""), c.get("title", "")))
doc.add_page_break()
img_ok = img_fail = 0
for c in chapters:
doc.add_heading("%s %s" % (c.get("chapter_no", ""), c.get("title", "")), level=1)
for line in (c.get("content") or "").split("\n"):
t = line.rstrip()
if not t:
continue
img = _parse_md_image(t)
if img:
alt, url = img
data = _fetch_image_bytes(url)
if data:
import io
pic_p = doc.add_paragraph()
pic_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
try:
pic_p.add_run().add_picture(io.BytesIO(data),
width=Inches(5.5))
if alt:
cap = doc.add_paragraph(alt)
cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
img_ok += 1
except Exception as e:
doc.add_paragraph("[配图 %s 嵌入失败:%s" % (alt or url[:60], str(e)[:80]))
img_fail += 1
else:
doc.add_paragraph("[配图缺失(下载失败):%s %s" % (alt or "", url[:80]))
img_fail += 1
continue
if t.startswith("### "):
doc.add_heading(t[4:], level=3)
elif t.startswith("## "):
@ -90,6 +159,8 @@ def _write_docx(path, title, subtitle, chapters):
doc.add_paragraph(t)
doc.add_page_break()
doc.save(path)
if img_ok or img_fail:
logger.info("compose: docx 配图嵌入 成功%d 失败%d", img_ok, img_fail)
return True, ""
except Exception as e:
return False, "%s: %s" % (type(e).__name__, str(e)[:200])