From c03469bf7a261a10e874600166fc0675945c044f Mon Sep 17 00:00:00 2001 From: ymq Date: Tue, 25 Aug 2026 21:27:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=98=BB=E5=A1=9E=E6=B1=82=E5=8A=A9?= =?UTF-8?q?=E4=B8=8D=E5=BE=97=E5=BD=93=E7=BC=BA=E9=99=B7=E9=A9=B3=E5=9B=9E?= =?UTF-8?q?=E2=80=94=E2=80=94bug=5Fflow=20=E5=89=8D=E7=BD=AE=E7=A1=AE?= =?UTF-8?q?=E5=AE=9A=E6=80=A7=E5=88=86=E6=B5=81=E8=BD=AC=20need=5Finfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回归修复(我引入):人事项目7 实测 deploy_test 两次 report_bug 上报「env/test.json 待确认无法部署」,被 pm_bug_confirm_run 在 9/13 秒内以「属测试环境问题,非代码缺陷」 自动 reject,PM 收不到求助 → 回退 requirement 重做 → requirement 同样拿不到环境信息 → 再编占位 → QC 再驳,死循环。 根因:prompt 判据写了「reject:测试环境问题导致的假失败」,LLM 严格照做。但 report_bug 在这里承担的是「向上冒泡求助」,不是「报告代码缺陷」。 修复分两层: 1) 代码层确定性分流(主):_is_blocking_help() 按特征词判定阻塞求助, 命中者不进 LLM,直接 _escalate_blocking_bugs() 转 need_info 冒泡给人, bug 保持 open 等信息补齐;幂等(已有 pending need_info 则跳过)。 2) prompt 兜底(次):删除「测试环境问题→reject」判据,加铁律说明外部输入 缺口不是可 reject 的非缺陷。 实测验证:2 个真实被误驳文本命中分流,3 个真缺陷文本不命中。 --- pipeline_service/bug_flow.py | 91 ++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/pipeline_service/bug_flow.py b/pipeline_service/bug_flow.py index a9f361b..f62f6c0 100644 --- a/pipeline_service/bug_flow.py +++ b/pipeline_service/bug_flow.py @@ -253,24 +253,104 @@ def _parse_bug_decisions(raw): return [] +_BLOCKING_HELP_PAT = re.compile( + r"待明确|待确认|待补齐|未提供|未配置|缺少环境|环境信息|无法\s*ssh|无法连接|连接不上|" + r"凭据|credentials|placeholder|\bTBD\b|需(要)?(人|PM|运维)(补|确认|提供)|" + r"占位|前置条件不具备|权限不足无法", + re.IGNORECASE) + + +def _is_blocking_help(bug): + """判定 bug 是否为「阻塞求助」而非「代码缺陷」。 + + 阻塞求助 = 缺少外部输入(环境信息/凭据/配置/权限)导致无法执行,只有人能解决。 + 这类必须转 need_info 冒泡给人,绝不能当「非缺陷」reject——否则求助通道被掐死: + 2026-08-25 人事项目7 实测,deploy_test 两次 report_bug 上报「env/test.json 待确认无法部署」, + 被 PM 确认流程在 9 秒内以「属测试环境问题,非代码缺陷」自动驳回,PM 收不到求助, + 转而回退 requirement 重做,而 requirement 同样拿不到环境信息 → 死循环。 + + 判定用确定性规则(特征词),不交给 LLM:是求助还是缺陷有明确特征,属于机制该管的部分。 + """ + text = f"{bug.get('title','')} {bug.get('description','') or ''}" + return bool(_BLOCKING_HELP_PAT.search(text)) + + +async def _escalate_blocking_bugs(sor, project_id, help_bugs): + """把「阻塞求助」类 bug 转成 need_info 冒泡给人,bug 保持 open 等人补信息。 + + 幂等:同一 bug 已有 pending 的 need_info 冒泡则跳过,避免 poller 每 20 秒刷一条。 + """ + from .communication import raise_problem + + escalated = 0 + for b in help_bugs: + bid = b.get('id', '') + if not bid: + continue + # 幂等:该 bug 已冒泡且未答结 → 跳过 + dup = await sor.sqlExe( + "SELECT id FROM pipeline_agent_questions " + "WHERE tenant_id=${pid}$ AND status='pending' AND problem_type='need_info' " + "AND question LIKE ${kw}$ LIMIT 1", + {"pid": project_id, "kw": f"%{bid}%"}) + await sor.sqlExe("COMMIT", {}) + if dup: + continue + + question = ( + f"【阻塞求助|需人工补充信息】Bug {bid}\n" + f"标题:{b.get('title','')}\n" + f"详情:{(b.get('description','') or '').strip()[:600]}\n\n" + f"该问题属于外部输入缺口(环境信息/凭据/配置/权限未提供)," + f"agent 无法自行解决,也不是代码缺陷。请补充所需信息后回答本问题," + f"流程将自动继续;Bug 保持 open 直至信息补齐。" + ) + try: + await raise_problem( + "need_info", question, from_role="agent.pm", + from_agentid="bug-flow", tenant_id=project_id, + task_id=b.get('task_id') or '', + first_handler_role="agent.main_agent", + suspend_task=bool(b.get('task_id'))) + escalated += 1 + logger.info(f"bug_flow 阻塞求助已冒泡给人 bug={bid} project={project_id}") + except Exception as e: + logger.warning(f"bug_flow 冒泡失败 bug={bid}: {e}") + return escalated + + async def pm_bug_confirm_run(sor, project_id): """PM 独立确认 open bug(open → confirmed/rejected)。 独立于任务审核循环:bug poller 扫到 open bug 时触发本流程,PM 读 open bug 清单 逐个判断「真缺陷(confirm) / 误报重复(reject)」,不依赖任务链任何环节。 只处理「进行中迭代」的 open bug。 + + 前置确定性分流:命中「阻塞求助」特征的 bug 不进 LLM 判断,直接转 need_info 冒泡给人, + bug 保持 open 等人补信息(不 reject、不 confirm)。 """ open_rows = await sor.sqlExe( - "SELECT b.id, b.title, b.description, b.severity, b.reporter_type, b.iteration_id " + "SELECT b.id, b.title, b.description, b.severity, b.reporter_type, b.iteration_id, b.task_id " "FROM sd_bugs b JOIN sd_iterations i ON b.iteration_id=i.id " "WHERE b.status='open' AND i.status='in_progress' AND i.project_id=${pid}$ " "ORDER BY b.created_at ASC LIMIT 50", {"pid": project_id}) await sor.sqlExe("COMMIT", {}) if not open_rows: - return {"confirmed": 0, "rejected": 0, "skipped": 0} + return {"confirmed": 0, "rejected": 0, "skipped": 0, "escalated": 0} + + all_bugs = [_rec_to_dict(r) for r in open_rows] + + # ── 前置分流:阻塞求助 → need_info 冒泡给人,不进 LLM ── + help_bugs = [b for b in all_bugs if _is_blocking_help(b)] + bugs = [b for b in all_bugs if not _is_blocking_help(b)] + escalated = 0 + if help_bugs: + escalated = await _escalate_blocking_bugs(sor, project_id, help_bugs) + + if not bugs: + return {"confirmed": 0, "rejected": 0, "skipped": 0, "escalated": escalated} - bugs = [_rec_to_dict(r) for r in open_rows] model, org_id = await _resolve_model_org(sor, project_id) bug_list = "\n".join( @@ -283,7 +363,10 @@ async def pm_bug_confirm_run(sor, project_id): "逐个判断每个 Bug:真缺陷 → confirm(确认,进入修复流程);误报/重复/非缺陷 → reject(驳回)。\n" "判断标准:\n" "- confirm:功能错误、代码缺陷、接口返回错误、字段/金额/权限/RBAC 校验错、表结构错、部署失败导致的真实代码问题。\n" - "- reject:测试环境问题导致的假失败、重复上报同一问题、非缺陷(如文案/样式已符合预期)。\n" + "- reject:重复上报同一问题、非缺陷(如文案/样式已符合预期)、经核实预期行为本就如此。\n" + "🔴 铁律:**缺少外部输入(环境信息/凭据/配置/权限未提供)导致无法执行,绝不是可 reject 的「非缺陷」**——" + "那是需要人补信息的阻塞求助,必须保留待处理,不得驳回。此类已由系统前置分流,你收到的列表里不应再有;" + "若仍见到,一律判 confirm 而非 reject。\n" "必须对每个 Bug 都给出 decision,不要遗漏。\n" "只输出一个 JSON 数组,格式:[{\"bug_id\":\"...\",\"decision\":\"confirm|reject\",\"comment\":\"一句话理由\"}]" )