feat(agent): 文档配图形态硬门禁——需求/设计文档及一切交付文档中的图必须invoke_model t2i/i2i生成真图(2026-09-15用户要求);新增diagram_gate确定性检测器(mermaid/plantuml围栏+箱线字符画++---+文本边框,豁免目录树条目/纯分隔线/markdown表格/代码块);deliver双路径(native+文本兜底)拦截伪图回填FAIL可行动改法;逃逸阀=文中如实标注「配图缺失:平台无可用文生图模型」;v1角色system prompt同步硬规定

This commit is contained in:
ymq 2026-09-15 14:53:17 +08:00
parent 313f32dd7b
commit af69708dd0
2 changed files with 157 additions and 0 deletions

View File

@ -601,6 +601,7 @@ __TOOLS__
## 配图与媒体生成(有平台模型可用,禁止字符画凑数)
产出文档/章节/方案时遇到需要插图的内容架构图/拓扑图/流程图示意图/效果图/封面图等 invoke_model 调用平台生成模型产出**真实图片**
- **需求/设计文档中的各种图必须真图2026-09-15 硬规定**四张图业务对象/生命周期/数据所有权/业务依赖系统架构图模块依赖图部署拓扑图界面效果图等一律 t2i/i2i 生成**禁止 mermaid/plantuml 代码块或 ASCII/文本框线图代替**QC 审查按此退回文字版要点可作图的补充说明图本体必须是生成图片
- 不确定有什么模型时先 list_platform_modelscapability=t2i 文生图 / i2v 图生视频 / tts 语音等
- invoke_model model 可留空平台按任务自动匹配task 写清画面内容主体/风格/构图/文字要求
- **完备性先于执行铁律**生成类调用真花钱 invoke_model 前先自查该能力的必需输入是否齐备文生图/视频要有画面描述图生视频/图生图要有输入图语音识别要有音频缺媒体素材时先 ask_question 向人类要拿到再调工具返回 QUESTION: 开头 = 平台门禁判定输入不完备未发生调用把追问原样转达给用户/按角色规则 ask_question禁止自行编造素材或空参硬试
@ -2210,6 +2211,33 @@ _FORCE_PRODUCE_HINT = (
)
def _validate_diagram_form(role, deliverable, space_dir, written_files):
"""交付件配图形态硬门禁2026-09-15 用户要求:文档中的图必须 t2i/i2i 真图)。
检查交付摘要result与本任务 write_file 实写的 .md 文档
出现 mermaid/plantuml 代码块或 ASCII/文本框线示意图= 拒绝 deliver
回填可行动 FAIL LLM 改用 invoke_model 生成真图后重交
确定性判定diagram_gate不靠 QC LLM 自觉平台无图像模型时产出中
如实标注配图缺失即豁免逃逸阀不逼死无图环境
返回 None=通过字符串=拒绝原因
"""
from .diagram_gate import find_fake_diagrams, gate_message
texts = [(deliverable.get("result") or "", "交付摘要")]
for f in dict.fromkeys(written_files or []):
if not f.lower().endswith(('.md', '.markdown')):
continue
try:
with open(f, encoding='utf-8', errors='ignore') as fh:
texts.append((fh.read(), os.path.relpath(f, space_dir) if f.startswith(space_dir) else f))
except Exception:
pass
for txt, where in texts:
problems = find_fake_diagrams(txt)
if problems:
return gate_message(problems, scene=where)
return None
def _validate_deliverable_type(role, deliverable_type, allowed_types):
"""deliver 类型守卫:角色声明了合法类型清单时,非法类型拒绝并返回清单(可行动报错)。
@ -2579,6 +2607,13 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": _dt_err})
logger.info(f"role_agent deliver type rejected: task={task_id} {_dt_err[:120]}")
continue
# 配图形态硬门禁2026-09-15文档中的图必须 t2i/i2i 真图,
# mermaid/ASCII 伪图拒绝 deliver回填 FAIL 让 LLM 改真图重交。
_dg_err = _validate_diagram_form(role, params, space_dir, written_files)
if _dg_err:
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": _dg_err})
logger.info(f"role_agent deliver diagram rejected: task={task_id} {_dg_err[:160]}")
continue
deliverable = {"action": "deliver", **params}
break
if tool == "ask_question":
@ -2628,6 +2663,13 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
msgs.append({"role": "user", "content": _dt_err})
logger.info(f"role_agent deliver type rejected(text): task={task_id} {_dt_err[:120]}")
continue
# 配图形态硬门禁文本兜底路径同款2026-09-15
_dg_err = _validate_diagram_form(role, act, space_dir, written_files)
if _dg_err:
msgs.append({"role": "assistant", "content": raw})
msgs.append({"role": "user", "content": _dg_err})
logger.info(f"role_agent deliver diagram rejected(text): task={task_id} {_dg_err[:160]}")
continue
deliverable = act
break
elif act.get('action') == 'ask':

View File

@ -0,0 +1,115 @@
"""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()])