116 lines
5.6 KiB
Python
116 lines
5.6 KiB
Python
"""pipeline_service.diagram_gate — 文档配图形态硬门禁(2026-09-15 用户要求)。
|
||
|
||
需求/设计文档(SDLC)与标书章节(投标)中的各种图**必须**用平台 t2i/i2i
|
||
模型生成真实图片(invoke_model →  嵌入正文)。
|
||
禁止 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 以  嵌入正文,删除 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()])
|