116 lines
5.6 KiB
Python
Raw 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.

"""pipeline_service.diagram_gate — 文档配图形态硬门禁(2026-09-15 用户要求)。
需求/设计文档(SDLC)与标书章节(投标)中的各种图**必须**用平台 t2i/i2i
模型生成真实图片(invoke_model → ![图名](产物URL) 嵌入正文)。
禁止 mermaid/plantuml 代码块、ASCII 字符画、文本框线拼「示意图」凑数。
判定是**确定性的**(花钱/质量闸门不靠 LLM 自觉,对齐 platform_model_tools
完备性门禁的分层原则):
- mermaid/plantuml/dot 围栏代码块 → 违规
- 制表符画(│├└┌─… 箱线字符 + 同行有实际内容)≥2 行 → 违规
- +---+ 文本边框 ≥2 行 → 违规
纯分隔线(整行只有 ─ 和空白)不算图,避免误伤。
唯一逃逸阀:平台无任何图像生成模型时,产出中如实标注
「配图缺失:平台无可用文生图模型」(DIAGRAM_MISSING_MARK),门禁放行。
消费方:
- agent_loop.role_agent_run deliver 拦截(SDLC requirement/design 角色)
- pipeline-bidding bid_write_capability.write_chapter / bid_review_capability.review_chapter
"""
import re
# 如实标注配图缺失的豁免标记(包含即放行,不逼死无图像模型的环境)
DIAGRAM_MISSING_MARK = "配图缺失"
# 需要真图的围栏代码块(画图 DSL 一律禁止)
_FENCE_RE = re.compile(r"```[ \t]*(?:mermaid|plantuml|puml|dot|graphviz)\b", re.I)
# 箱线字符(制表符画/框图常用)
_BOX_CHARS = set("│├└┌┐┘┬┴┤┼╔╗╚╝╠╣╦╩╬═║─━┃┏┓┗┛")
# 角/连接字符:出现即判定为箱线图边框行(纯 ─ 分隔线无角字符,不误伤)
_CORNER_CHARS = set("┌┐└┘┏┓┗┛╔╗╚╝├┤┬┴┼╠╣╦╩╬")
# +----+ 风格文本边框
_PLUS_BOX_RE = re.compile(r"\+[-=]{3,}\+")
def _is_tree_entry(line):
"""目录树条目行(├── src/main.py 这类):箱线字符集中在行首前缀,
其后是非空的普通文本(无箱线字符穿插)→ 是文本清单不是图,豁免。
整行全是箱线字符(┏━━━━┓)不是目录树条目(是图边框)。
反例(判为图):├─组合─ 工单消息——箱线字符与内容穿插。"""
s = line
i = 0
# 跳过行首的箱线字符与空白(目录树缩进前缀可含 │ + 空格,如 │ ├── name)
while i < len(s) and (s[i] in _BOX_CHARS or s[i].isspace()):
i += 1
if i == 0:
return False
rest = s[i:]
if not rest.strip():
return False # 整行全是箱线字符/空白(如 ┏━━━━━━┓)→ 是图边框,非目录条目
return not any(ch in _BOX_CHARS for ch in rest)
def _is_box_line(line):
"""该行是否是箱线画的组成部分(确定性判定,纯 ──── 分隔线不算图,防误伤):
- 含任一角/连接字符(┌┐└┘┏┓┗┛├┤┬┴┼…)→ 是边框/树形线;或
- ≥2 个箱线字符且除箱线/空白外还有 ≥2 个实际内容字符(如 ├─组合─ 工单)。
豁免:目录树条目行(├── src/ 这类「箱线字符+单个词」)是文本清单不是图。"""
n_box = 0
n_other = 0
for ch in line:
if ch in _BOX_CHARS:
n_box += 1
elif not ch.isspace():
n_other += 1
if any(ch in _CORNER_CHARS for ch in line):
# 目录树条目豁免:├── name / └── name/ / │ ├── name(箱线后只有普通文本)
if _is_tree_entry(line):
return False
return True
return n_box >= 2 and n_other >= 2
def find_fake_diagrams(text):
"""检测文本中的伪图,返回违规描述列表(空 = 通过)。确定性判定,无语义猜测。"""
problems = []
if not text:
return problems
if DIAGRAM_MISSING_MARK in text:
# 如实标注平台无图像模型 → 逃逸阀放行(标注真实性由 QC/评审按平台模型清单核对)
return problems
if _FENCE_RE.search(text):
problems.append("mermaid/plantuml 画图代码块")
lines = text.splitlines()
box_lines = sum(1 for ln in lines if _is_box_line(ln))
if box_lines >= 2:
problems.append("ASCII/箱线字符画示意图(%d 行)" % box_lines)
plus_lines = sum(1 for ln in lines if _PLUS_BOX_RE.search(ln))
if plus_lines >= 2:
problems.append("+---+ 文本边框示意图(%d 行)" % plus_lines)
return problems
def gate_message(problems, scene="文档"):
"""统一的可行动拒绝话术(说清怎么改,不是只说不行)。"""
return ("FAIL: %s中的图必须用平台 t2i/i2i 模型生成真实图片(2026-09-15 硬规定)。"
"发现伪图:%s。请逐图改用 invoke_model 生成"
"(model 留空自动选型;task 写清图的主体/层次/风格——商务风、白底、含中文标注——"
"及图中文字要求;改造已有图带输入图 URL 走 i2i),"
"产物 URL 以 ![图名](URL) 嵌入正文,删除 mermaid/ASCII 字符画后重新提交。"
"仅当平台确无任何图像生成模型时,才可在文中如实标注「%s:平台无可用文生图模型」。"
% (scene, ";".join(sorted(set(problems))), DIAGRAM_MISSING_MARK))
def count_image_embeds(text):
"""统计 markdown 图片嵌入行数(![...](...),排除空 URL)。正向门禁用:
要求含四张图/架构图的文档至少有一张真图。"""
if not text:
return 0
return len([m for m in re.finditer(r"!\[[^\]]*\]\(([^)\s]+)\)", text)
if m.group(1).strip()])