fix(compose): 标书docx生成两连修(2026-09-16甘肃项目实测)——①pyproject声明python-docx依赖(原dependencies=[]:测试机无docx模块→_write_docx ImportError诚实降级只出md,但标书最终交付必须docx;与pricing漏声明pyyaml同款教训) ②_fetch_image_bytes对/idfile|/download网关URL直接映射本机filesroot读盘(合成进程无登录态,HTTP拉logined-only的/idfile必401→配图全变'下载失败'文字占位;防穿越realpath校验)

This commit is contained in:
yumoqing 2026-09-16 14:50:49 +08:00
parent 842023772d
commit 9a7dfef204
2 changed files with 45 additions and 6 deletions

View File

@ -40,27 +40,63 @@ def _fetch_image_bytes(url, timeout=30):
URL invoke_model 产物经 downloadfile2url 落地的本地持久地址
/idfile 静态路径上游 24h 时效 URL 已在推理层落地这里拿到的是
平台自身地址http(s) 下载本地绝对路径直读失败返回 None诚实
降级docx 里保留文字说明不假装成功
平台自身地址
解析优先级2026-09-16 修复
1. /idfile|/download 网关 URL 直接映射本机 filesroot 读盘合成进程无
登录态HTTP logined-only /idfile 401图全变下载失败占位
甘肃项目 bid_v1 docx 实测根因
2. 本地绝对路径直读
3. 其余 http(s) 下载
失败返回 None诚实降级docx 里保留文字说明不假装成功
"""
try:
u = (url or '').strip()
if not u:
return None
# 网关 URL → filesroot 本地映射
if u.startswith('http://') or u.startswith('https://'):
from urllib.parse import urlparse, unquote
path = unquote(urlparse(u).path or '')
for lead in ('/idfile/', '/download/'):
if path.startswith(lead):
local = _filesroot_path(path[len(lead):])
if local and os.path.isfile(local):
with open(local, 'rb') as f:
return f.read()
break
if u.startswith('/') and os.path.isfile(u):
with open(u, 'rb') as f:
return f.read()
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
def _filesroot_path(rel):
"""/idfile 网关相对路径 → 本机 filesroot 绝对路径防穿越realpath 校验)。"""
try:
from appPublic.jsonConfig import getConfig
root = getattr(getConfig(), 'filesroot', '') or ''
if not root:
return ''
rel = (rel or '').lstrip('/')
if not rel or '..' in rel.split('/'):
return ''
full = os.path.realpath(os.path.join(root, rel))
if full.startswith(os.path.realpath(root) + os.sep):
return full
except Exception:
pass
return ''
# markdown 图片行:![alt](url)(章节写作者按角色 prompt 规范嵌入 invoke_model 产物)
_MD_IMG_RE = None

View File

@ -2,7 +2,10 @@
name = "pipeline_bidding"
version = "0.1.0"
description = "投标产线:招标信息采集→审批→立项→解析→资质准备→分章节编写→评审→合成→评分→交付"
dependencies = []
# ⚠ python-docx 必须声明compose_bid 合成标书 docx 依赖它,缺了走 ImportError
# 诚实降级只出 md2026-09-16 甘肃项目实测:标书最终要 docx 交付,静默降级不达标;
# 与 pricing 漏声明 pyyaml 同款教训——非 editable 环境 pip install 不带依赖必炸)
dependencies = ["python-docx"]
[tool.setuptools.packages.find]
where = ["."]