diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index 36ed8d3..e579bcf 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -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