fix(agent): G2缺口指纹超长静默丢失+fallback交付件跨仓库污染+setup_repos空目录崩溃

- _check_orchestration_gaps: G1环/G2指纹改短哈希(_short_fingerprint)——原实现拼接全部未依赖任务ID(15任务≈400字符)超pipeline_pm_notices.fingerprint VARCHAR(255)→INSERT DataError(1406)被catch记debug→PM告警静默丢失,脚手架先批deploy_test提前启动(pbls 09-15实测)
- _build_fallback_deliverable: git扫描只报本任务write_file写入过的仓库——原实现扫全工作空间apps/modules所有git仓库,16天前元景遗留的apps/scense脏工作区被算进M1a交付件→QC误判越权改动构成退回理由(pbls实测)
- _setup_repos: project_dir空串守卫——os.makedirs('')抛Errno 2且中断clone,日志高频刷屏
This commit is contained in:
ymq 2026-09-15 16:43:23 +08:00
parent 5fe5c238ec
commit e774cb238b

View File

@ -1418,7 +1418,12 @@ async def _commit_repos_after_approve(space_dir, project_name='', title=''):
async def _setup_repos(sor, space_dir, project_id):
"""clone 项目关联仓库到机构工作空间应用→apps/、模块→modules/项目目录→projects/{项目名}/。"""
project_dir = await _get_project_dir(sor, project_id)
await _ensure_project_repo(project_dir)
# 守卫取不到项目目录sd_projects 记录缺 directory_name/name或 space_dir 解析失败)时
# 原实现把空串传进 _ensure_project_repo → os.makedirs('') 抛
# FileNotFoundError [Errno 2] No such file or directory: ''(日志高频刷屏,
# 且整个 setup_repos 中断,后续 clone 也不执行)。空目录直接跳过项目仓库步骤。
if project_dir:
await _ensure_project_repo(project_dir)
repos = await _get_project_repos(sor, project_id)
results = []
for repo in repos:
@ -1587,7 +1592,7 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
continue
if color[m] == GRAY:
cyc = path[path.index(m):] if m in path else [m]
fp = "G1:cycle:" + ",".join(sorted(cyc))
fp = _short_fingerprint("G1:cycle", *sorted(cyc))
if fp in _cyc_seen:
continue
_cyc_seen.add(fp)
@ -1617,7 +1622,7 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
if miss_tasks:
miss_ids = [m['id'] for m in miss_tasks]
gaps.append({
'kind': 'G2', 'fingerprint': "G2:" + a['id'] + ":" + ",".join(sorted(miss_ids)),
'kind': 'G2', 'fingerprint': _short_fingerprint("G2", a['id'], *sorted(miss_ids)),
'task_id': a['id'], 'fix_ids': miss_ids,
'text': (f"G2 应用级 develop「{a['title']}」({a['id']}) 未依赖以下未完成的模块任务:"
+ "".join(f"{m['title']}({m['id']})" for m in miss_tasks)
@ -1654,6 +1659,21 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
return gaps
def _short_fingerprint(prefix, *parts):
"""缺口指纹定长化prefix + 内容 md5 前 16 位。
2026-09-15 pbls 实测G2 指纹把全部未依赖任务 ID 拼进字符串15 个任务 400 字符
超出 pipeline_pm_notices.fingerprint VARCHAR(255) INSERT DataError(1406)
_save_gap_notices catch debug 告警静默丢失PM 从未看到
应用级 develop 未依赖模块任务 脚手架先批deploy_test 在编码未完成时启动
正是 G2 要防的事故指纹只服务 (tenant_id, fingerprint) 去重不需要可读
哈希后长度恒定与依赖数量无关
"""
import hashlib
body = ":".join(str(p) for p in parts if p)
return f"{prefix}:{hashlib.md5(body.encode('utf-8')).hexdigest()[:16]}"
def _gap_texts(gaps):
"""结构化缺口 → 纯文本列表(日志用)。"""
return [g['text'] for g in (gaps or []) if isinstance(g, dict) and g.get('text')]
@ -2263,32 +2283,41 @@ def _validate_deliverable_type(role, deliverable_type, allowed_types):
f"(如 {allowed_types[0]})。")
async def _build_fallback_deliverable(space_dir, written_files, role):
"""循环结束未 deliver 时,检测 write_file/git 实际产出,构造真实交付件给 QC而非占位符「Agent未产出交付件」"""
async def _build_fallback_deliverable(space_dir, written_files, role, task_id=''):
"""循环结束未 deliver 时,检测 write_file/git 实际产出,构造真实交付件给 QC而非占位符「Agent未产出交付件」
2026-09-15 pbls 修复git 扫描只报告本任务 write_file 实际写入过的仓库
原实现把机构工作空间 apps/ + modules/ 下所有 git 仓库的脏状态都塞进交付件
历史遗留的脏工作区实测 apps/scense 12 项变更全是 16 天前别的任务留下的
被当成本任务产出 QC 误判越权改动其它应用仓库构成独立退回理由
本任务没写过文件的仓库与本任务无关不进交付件
"""
lines = []
if written_files:
lines.append("本任务执行期间 write_file 实际写入的文件:")
for f in sorted(set(written_files)):
for f in sorted(written_files):
rel = os.path.relpath(f, space_dir) if f.startswith(space_dir) else f
lines.append("- " + rel)
# 扫描 apps/ + modules/ + projects/ 下的 git 仓库工作区变更
for sub in ('apps', 'modules', 'projects'):
base = os.path.join(space_dir, sub)
if not os.path.isdir(base):
# 只扫描本任务实际写入过的仓库write_file 落点所属),不扫全工作空间
touched_repos = set()
for f in written_files or []:
rp = f if os.path.isabs(f) else os.path.join(space_dir, f)
rel = os.path.relpath(rp, space_dir)
parts = rel.split(os.sep)
if len(parts) >= 2 and parts[0] in ('apps', 'modules', 'projects'):
touched_repos.add(os.path.join(space_dir, parts[0], parts[1]))
for rp in sorted(touched_repos):
if not os.path.isdir(os.path.join(rp, '.git')):
continue
entries = ([(p, p) for p in sorted(os.listdir(base))] if sub == 'projects'
else [(f"{sub}/{n}", n) for n in sorted(os.listdir(base))])
for label, name in entries:
rp = os.path.join(base, name)
if os.path.isdir(os.path.join(rp, '.git')):
try:
r = await _run_shell('git status --short', rp, 10)
out = (r.get('stdout') or '').strip()
if out:
lines.append(f"git 仓库 [{label}] 工作区变更:")
lines.append(out[:2000])
except Exception:
pass
try:
r = await _run_shell('git status --short', rp, 10)
out = (r.get('stdout') or '').strip()
if out:
label = os.path.relpath(rp, space_dir)
lines.append(f"git 仓库 [{label}] 工作区变更(仅本任务写入过的仓库):")
lines.append(out[:2000])
except Exception:
pass
if not lines:
lines.append("(本轮未检测到 write_file 或 git 变更——agent 确实未产出)")
return {
@ -2718,7 +2747,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
if not deliverable:
# 兜底:循环结束未 deliver检测 write_file/git 实际产出构造真实交付件(而非占位符)
deliverable = await _build_fallback_deliverable(space_dir, written_files, role)
deliverable = await _build_fallback_deliverable(space_dir, written_files, role, task_id=task_id)
# ── 处理产出 ──
from appPublic.uniqueID import getID