diff --git a/pipeline_bidding/bid_compose_capability.py b/pipeline_bidding/bid_compose_capability.py index e614579..4d9ca4e 100644 --- a/pipeline_bidding/bid_compose_capability.py +++ b/pipeline_bidding/bid_compose_capability.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 0c90dcd..754fab6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,10 @@ name = "pipeline_bidding" version = "0.1.0" description = "投标产线:招标信息采集→审批→立项→解析→资质准备→分章节编写→评审→合成→评分→交付" -dependencies = [] +# ⚠ python-docx 必须声明:compose_bid 合成标书 docx 依赖它,缺了走 ImportError +# 诚实降级只出 md(2026-09-16 甘肃项目实测:标书最终要 docx 交付,静默降级不达标; +# 与 pricing 漏声明 pyyaml 同款教训——非 editable 环境 pip install 不带依赖必炸) +dependencies = ["python-docx"] [tool.setuptools.packages.find] where = ["."]