From af69708dd05603271323f0db4c5ce57cf64acf3d Mon Sep 17 00:00:00 2001 From: ymq Date: Tue, 15 Sep 2026 14:53:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=96=87=E6=A1=A3=E9=85=8D?= =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E6=80=81=E7=A1=AC=E9=97=A8=E7=A6=81=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=9C=80=E6=B1=82/=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=8F=8A=E4=B8=80=E5=88=87=E4=BA=A4=E4=BB=98=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=AD=E7=9A=84=E5=9B=BE=E5=BF=85=E9=A1=BBinvoke=5F?= =?UTF-8?q?model=20t2i/i2i=E7=94=9F=E6=88=90=E7=9C=9F=E5=9B=BE(2026-09-15?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=A6=81=E6=B1=82);=E6=96=B0=E5=A2=9Ediagram?= =?UTF-8?q?=5Fgate=E7=A1=AE=E5=AE=9A=E6=80=A7=E6=A3=80=E6=B5=8B=E5=99=A8(m?= =?UTF-8?q?ermaid/plantuml=E5=9B=B4=E6=A0=8F+=E7=AE=B1=E7=BA=BF=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E7=94=BB++---+=E6=96=87=E6=9C=AC=E8=BE=B9=E6=A1=86,?= =?UTF-8?q?=E8=B1=81=E5=85=8D=E7=9B=AE=E5=BD=95=E6=A0=91=E6=9D=A1=E7=9B=AE?= =?UTF-8?q?/=E7=BA=AF=E5=88=86=E9=9A=94=E7=BA=BF/markdown=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC/=E4=BB=A3=E7=A0=81=E5=9D=97);deliver=E5=8F=8C?= =?UTF-8?q?=E8=B7=AF=E5=BE=84(native+=E6=96=87=E6=9C=AC=E5=85=9C=E5=BA=95)?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E4=BC=AA=E5=9B=BE=E5=9B=9E=E5=A1=ABFAIL?= =?UTF-8?q?=E5=8F=AF=E8=A1=8C=E5=8A=A8=E6=94=B9=E6=B3=95;=E9=80=83?= =?UTF-8?q?=E9=80=B8=E9=98=80=3D=E6=96=87=E4=B8=AD=E5=A6=82=E5=AE=9E?= =?UTF-8?q?=E6=A0=87=E6=B3=A8=E3=80=8C=E9=85=8D=E5=9B=BE=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?:=E5=B9=B3=E5=8F=B0=E6=97=A0=E5=8F=AF=E7=94=A8=E6=96=87?= =?UTF-8?q?=E7=94=9F=E5=9B=BE=E6=A8=A1=E5=9E=8B=E3=80=8D;v1=E8=A7=92?= =?UTF-8?q?=E8=89=B2system=20prompt=E5=90=8C=E6=AD=A5=E7=A1=AC=E8=A7=84?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_service/agent_loop.py | 42 +++++++++++ pipeline_service/diagram_gate.py | 115 +++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 pipeline_service/diagram_gate.py diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index c8f4616..bbd88fc 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -601,6 +601,7 @@ __TOOLS__ ## 配图与媒体生成(有平台模型可用,禁止字符画凑数) 产出文档/章节/方案时,遇到需要插图的内容(架构图/拓扑图/流程图示意图/效果图/封面图等),用 invoke_model 调用平台生成模型产出**真实图片**: +- **需求/设计文档中的各种图必须真图(2026-09-15 硬规定)**:四张图(业务对象/生命周期/数据所有权/业务依赖)、系统架构图、模块依赖图、部署拓扑图、界面效果图等,一律 t2i/i2i 生成,**禁止 mermaid/plantuml 代码块或 ASCII/文本框线图代替**(QC 审查按此退回)。文字版要点可作图的补充说明,图本体必须是生成图片。 - 不确定有什么模型时先 list_platform_models(capability=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': diff --git a/pipeline_service/diagram_gate.py b/pipeline_service/diagram_gate.py new file mode 100644 index 0000000..5f12c74 --- /dev/null +++ b/pipeline_service/diagram_gate.py @@ -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()])